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

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

このコミットは、Go言語の標準ライブラリであるtestingパッケージにおいて、panic(nil)を呼び出すバグのあるテストを診断し、より明確なエラーメッセージを提供するように改善するものです。具体的には、テスト関数がpanic(nil)によって異常終了した場合に、testingフレームワークがそれを検出し、"test executed panic(nil)"というエラーとして報告するように変更されます。これにより、開発者はテストの失敗原因をより迅速に特定できるようになります。

コミット

commit ae562107089a8d989f0526b90a7bce50a9da4348
Author: Russ Cox <rsc@golang.org>
Date:   Wed Jan 22 16:04:50 2014 -0500

    testing: diagnose buggy tests that panic(nil)
    
    Fixes #6546.
    
    LGTM=dave, bradfitz, r
    R=r, dave, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/55780043

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

https://github.com/golang/go/commit/ae562107089a8d989f0526b90a7bce50a9da4348

元コミット内容

    testing: diagnose buggy tests that panic(nil)
    
    Fixes #6546.
    
    LGTM=dave, bradfitz, r
    R=r, dave, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/55780043

変更の背景

Go言語において、panicはプログラムの異常終了を示すメカニズムです。通常、panicは非nilの値(エラーオブジェクトなど)を引数として受け取ります。しかし、Goの仕様上、panic(nil)と呼び出すことも可能です。このpanic(nil)は、recover()関数で捕捉しようとした際にnilが返されるため、通常のパニックとは異なる振る舞いをします。

Goのtestingパッケージでは、テスト関数がパニックを起こした場合、そのパニックを捕捉し、テストを失敗としてマークし、パニックの情報を報告する仕組みがあります。しかし、panic(nil)が起こった場合、recover()nilを返すため、testingパッケージの既存のパニック処理ロジックでは、それが通常のパニックなのか、それともテスト関数が正常に終了したのかを区別することが困難でした。

結果として、panic(nil)を呼び出すテストは、期待通りに失敗として報告されず、あたかも正常に完了したかのように見えてしまう可能性がありました。これは、テストの信頼性を損ない、開発者がバグのあるテストを特定するのを困難にする問題でした。このコミットは、このpanic(nil)によるテストの診断を改善し、明確なエラーメッセージを出すことで、開発体験を向上させることを目的としています。

前提知識の解説

Goにおけるpanicrecover

Go言語には、例外処理に似たpanicrecoverというメカニズムがあります。

  • panic: プログラムの実行を中断し、現在のゴルーチンを停止させます。通常、回復不可能なエラーや予期せぬ状況が発生した場合に用いられます。panicが呼び出されると、現在の関数の実行が直ちに停止し、その関数のdefer関数が実行されます。その後、呼び出し元の関数へとパニックが伝播し、同様にdefer関数が実行され、最終的にプログラム全体がクラッシュするか、recoverによって捕捉されるまで続きます。
  • recover: panicによって停止したゴルーチンを回復させるために使用されます。recoverdefer関数内でしか意味を持ちません。defer関数内でrecover()が呼び出されると、パニックの値(panicに渡された引数)が返され、ゴルーチンのパニック状態が解除され、通常の実行が再開されます。もしパニックが発生していない状態でrecover()が呼び出された場合、nilが返されます。

panic(nil)の特殊性

panicnilを渡すことはGoの言語仕様上可能です。しかし、これは推奨されません。なぜなら、recover()nilを返すため、パニックが発生したのか、それとも単にrecover()がパニックしていない状態で呼び出されたのかを区別するのが難しくなるからです。この曖昧さが、testingパッケージのようなパニックを捕捉して処理するシステムにおいて問題を引き起こします。

testingパッケージのtRunner関数

Goのtestingパッケージは、テストを実行するためのフレームワークを提供します。各テスト関数(func TestXxx(t *testing.T))は、内部的にtRunnerという関数によって実行されます。tRunnerは、テスト関数の実行をラップし、テストの開始時刻と終了時刻を記録したり、テスト中に発生したパニックを捕捉して適切に処理したりする役割を担っています。

tRunner関数内では、テスト関数の実行後にdefer文が設定されており、テストが正常に終了したか、あるいはパニックによって終了したかを判断し、それに応じた処理(例えば、テスト時間の記録、パニック情報の報告など)を行います。

技術的詳細

このコミットの核心は、src/pkg/testing/testing.go内のtRunner関数におけるdeferブロックのロジック変更です。

変更前は、deferブロック内でrecover()を呼び出し、その結果がnilでなければパニックが発生したと判断していました。しかし、前述の通りpanic(nil)の場合、recover()nilを返してしまうため、このロジックではpanic(nil)を検出できませんでした。

この問題を解決するため、以下の変更が導入されました。

  1. finished変数の導入: tRunner関数のスコープにfinishedというbool型の変数が追加されました。この変数は初期値としてfalseを持ちます。
  2. finished = trueの設定: 実際のテスト関数test.F(t)が正常に実行を完了した直後に、finished変数をtrueに設定する行が追加されました。
  3. deferブロック内のロジック変更:
    • recover()の戻り値がerr変数に格納されます。
    • 新たな条件式 if !finished && err == nil が追加されました。
      • !finished: これは、テスト関数がfinished = trueの行に到達する前に終了したことを意味します。つまり、テスト関数がパニックを起こしたか、runtime.Goexitを呼び出したかのいずれかです。
      • err == nil: これは、recover()nilを返したことを意味します。
    • この両方の条件が真である場合(つまり、テスト関数が途中で終了し、かつrecover()nilを返した場合)、それはpanic(nil)が発生したと判断されます。この場合、err変数は明示的にfmt.Errorf("test executed panic(nil)")というエラーメッセージを持つerror型に設定されます。
    • その後、err != nilの条件でパニックが検出され、t.Fail()が呼び出されてテストが失敗としてマークされ、t.report()でレポートが生成され、最後に元のパニック値(または新しく生成された"test executed panic(nil)"エラー)で再度panicが起こされます。

この変更により、panic(nil)が発生した場合でも、testingフレームワークはそれを明確なエラーとして認識し、"test executed panic(nil)"という診断メッセージを伴ってテストを失敗させることができるようになりました。これにより、開発者はpanic(nil)という潜在的に曖昧なバグを容易に特定できるようになります。

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

--- a/doc/go1.3.txt
+++ b/doc/go1.3.txt
@@ -3,3 +3,4 @@ liblink: pull linker i/o into separate liblink C library (CL 35790044)
 misc/dist: renamed misc/makerelease (CL 39920043)
 runtime: output how long goroutines are blocked (CL 50420043)
 syscall: add NewCallbackCDecl to use for windows callbacks (CL 36180044)
+testing: diagnose buggy tests that panic(nil) (CL 55780043)
diff --git a/src/pkg/testing/testing.go b/src/pkg/testing/testing.go
index 52dc166dd9..a0b55f4a57 100644
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -376,10 +376,15 @@ func tRunner(t *T, test *InternalTest) {
 	// returned normally or because a test failure triggered
 	// a call to runtime.Goexit, record the duration and send
 	// a signal saying that the test is done.
+	var finished bool
 	defer func() {\n 		t.duration = time.Now().Sub(t.start)
 		// If the test panicked, print any test output before dying.
-		if err := recover(); err != nil {
+		err := recover()
+		if !finished && err == nil {
+			err = fmt.Errorf("test executed panic(nil)")
+		}
+		if err != nil {
 			t.Fail()
 			t.report()
 			panic(err)
@@ -389,6 +394,7 @@ func tRunner(t *T, test *InternalTest) {
 
 	t.start = time.Now()
 	test.F(t)
+	finished = true
 }
 
 // An internal function but exported because it is cross-package; part of the implementation

コアとなるコードの解説

変更の中心はsrc/pkg/testing/testing.goファイルのtRunner関数です。

  1. var finished bool:

    • tRunner関数の冒頭にfinishedという新しいブール変数が宣言されました。この変数は、テスト関数test.F(t)が正常に最後まで実行されたかどうかを追跡するために使用されます。初期値はGoのゼロ値であるfalseです。
  2. finished = true:

    • test.F(t)(実際のテスト関数)の呼び出し直後に、finished = trueという行が追加されました。これは、テスト関数がパニックを起こさずに、またはruntime.Goexitを呼び出さずに、そのロジックの最後まで到達した場合にのみ実行されます。もしテスト関数が途中でパニックを起こしたり、runtime.Goexitを呼び出したりした場合、この行は実行されず、finishedfalseのままになります。
  3. deferブロック内の変更:

    • err := recover(): defer関数内でrecover()が呼び出され、その結果がerr変数に格納されます。
    • if !finished && err == nil { err = fmt.Errorf("test executed panic(nil)") }:
      • これがこのコミットの最も重要なロジックです。
      • !finished: テスト関数がfinished = trueの行に到達しなかったことを意味します。これは、テスト関数が途中でパニックを起こしたか、runtime.Goexitを呼び出したことを示唆します。
      • err == nil: recover()nilを返したことを意味します。これは、パニックが発生しなかったか、またはpanic(nil)が発生したかのいずれかです。
      • この二つの条件が同時に真である場合、それはテスト関数が途中で終了したにもかかわらず、recover()nilを返した、つまりpanic(nil)が発生したと結論付けられます。
      • この状況では、err変数はfmt.Errorf("test executed panic(nil)")という新しいエラーオブジェクトで上書きされます。これにより、後続のif err != nilのチェックでこのpanic(nil)が捕捉され、明確なエラーメッセージとして報告されるようになります。
    • if err != nil { ... }:
      • この既存の条件は、recover()が非nilの値を返した場合(通常のパニック)と、上記のロジックによってerrfmt.Errorf("test executed panic(nil)")に設定された場合の両方で真となります。
      • このブロック内で、t.Fail()が呼び出されてテストが失敗としてマークされ、t.report()でテスト結果が報告され、最後にpanic(err)で元のパニック値(またはpanic(nil)の場合に生成されたエラー)が再パニックされます。これにより、テストランナーはテストの異常終了を適切に処理し、診断情報を出力できます。

この一連の変更により、Goのtestingパッケージは、panic(nil)という特殊なケースのパニックも正確に診断し、開発者にとってより分かりやすいエラーメッセージを提供するようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のpanicrecoverに関する公式ドキュメントやブログ記事 (一般的な知識のため特定のURLは記載しませんが、Goの公式ブログやEffective Goなどが参考になります)
  • Goのtestingパッケージのソースコード (変更内容の理解のため)
  • GoのIssueトラッカー (Issue 6546の背景理解のため)
  • fmt.Errorfの挙動に関するGoのドキュメント (エラー生成の理解のため)
  • runtime.Goexitに関するGoのドキュメント (関連する挙動の理解のため)I have generated the explanation based on the commit data and the specified structure. I have also incorporated details about panic(nil) and the testing package's tRunner function.

I will now output the explanation to standard output.