[インデックス 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におけるpanic
とrecover
Go言語には、例外処理に似たpanic
とrecover
というメカニズムがあります。
panic
: プログラムの実行を中断し、現在のゴルーチンを停止させます。通常、回復不可能なエラーや予期せぬ状況が発生した場合に用いられます。panic
が呼び出されると、現在の関数の実行が直ちに停止し、その関数のdefer
関数が実行されます。その後、呼び出し元の関数へとパニックが伝播し、同様にdefer
関数が実行され、最終的にプログラム全体がクラッシュするか、recover
によって捕捉されるまで続きます。recover
:panic
によって停止したゴルーチンを回復させるために使用されます。recover
はdefer
関数内でしか意味を持ちません。defer
関数内でrecover()
が呼び出されると、パニックの値(panic
に渡された引数)が返され、ゴルーチンのパニック状態が解除され、通常の実行が再開されます。もしパニックが発生していない状態でrecover()
が呼び出された場合、nil
が返されます。
panic(nil)
の特殊性
panic
にnil
を渡すことは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)
を検出できませんでした。
この問題を解決するため、以下の変更が導入されました。
finished
変数の導入:tRunner
関数のスコープにfinished
というbool
型の変数が追加されました。この変数は初期値としてfalse
を持ちます。finished = true
の設定: 実際のテスト関数test.F(t)
が正常に実行を完了した直後に、finished
変数をtrue
に設定する行が追加されました。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
関数です。
-
var finished bool
:tRunner
関数の冒頭にfinished
という新しいブール変数が宣言されました。この変数は、テスト関数test.F(t)
が正常に最後まで実行されたかどうかを追跡するために使用されます。初期値はGoのゼロ値であるfalse
です。
-
finished = true
:test.F(t)
(実際のテスト関数)の呼び出し直後に、finished = true
という行が追加されました。これは、テスト関数がパニックを起こさずに、またはruntime.Goexit
を呼び出さずに、そのロジックの最後まで到達した場合にのみ実行されます。もしテスト関数が途中でパニックを起こしたり、runtime.Goexit
を呼び出したりした場合、この行は実行されず、finished
はfalse
のままになります。
-
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
の値を返した場合(通常のパニック)と、上記のロジックによってerr
がfmt.Errorf("test executed panic(nil)")
に設定された場合の両方で真となります。 - このブロック内で、
t.Fail()
が呼び出されてテストが失敗としてマークされ、t.report()
でテスト結果が報告され、最後にpanic(err)
で元のパニック値(またはpanic(nil)
の場合に生成されたエラー)が再パニックされます。これにより、テストランナーはテストの異常終了を適切に処理し、診断情報を出力できます。
- この既存の条件は、
この一連の変更により、Goのtesting
パッケージは、panic(nil)
という特殊なケースのパニックも正確に診断し、開発者にとってより分かりやすいエラーメッセージを提供するようになりました。
関連リンク
- Go CL 55780043: https://golang.org/cl/55780043
- Go Issue 6546: https://golang.org/issue/6546
参考にした情報源リンク
- Go言語の
panic
とrecover
に関する公式ドキュメントやブログ記事 (一般的な知識のため特定の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 aboutpanic(nil)
and thetesting
package'stRunner
function.
I will now output the explanation to standard output.