[インデックス 11635] ファイルの概要
このコミットは、Go言語の標準ライブラリであるtesting
パッケージにおける重要な改善を導入しています。具体的には、テスト実行中に発生したパニック(panic)を捕捉し、その情報をテスト結果として表示するとともに、該当するテストを失敗としてマークする機能を追加しています。これにより、テストの堅牢性が向上し、予期せぬパニックによるテストの中断を防ぎ、問題の特定を容易にしています。
コミット
testing: capture panics, present them, and mark the test as a failure.
R=r
CC=golang-dev
https://golang.org/cl/5633044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cee920225ddaec164c0026480e072e0ea568db40
元コミット内容
commit cee920225ddaec164c0026480e072e0ea568db40
Author: David Symonds <dsymonds@golang.org>
Date: Mon Feb 6 14:00:23 2012 +1100
testing: capture panics, present them, and mark the test as a failure.
R=r
CC=golang-dev
https://golang.org/cl/5633044
---
src/pkg/testing/testing.go | 20 ++++++++++++++++++++\n 1 file changed, 20 insertions(+)
diff --git a/src/pkg/testing/testing.go b/src/pkg/testing/testing.go
index f1acb97e1b..68ecebb36f 100644
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -225,6 +225,19 @@ func (c *common) Fatalf(format string, args ...interface{}) {
c.FailNow()
}
+// TODO(dsymonds): Consider hooking into runtime·traceback instead.
+func (c *common) stack() {
+ for i := 2; ; i++ { // Caller we care about is the user, 2 frames up
+ pc, file, line, ok := runtime.Caller(i)
+ f := runtime.FuncForPC(pc)
+ if !ok || f == nil {
+ break
+ }
+ c.Logf("%s:%d (0x%x)", file, line, pc)
+ c.Logf("\t%s", f.Name())
+ }
+}
+
// Parallel signals that this test is to be run in parallel with (and only with)
// other parallel tests in this CPU group.
func (t *T) Parallel() {
@@ -247,6 +260,13 @@ func tRunner(t *T, test *InternalTest) {
// a call to runtime.Goexit, record the duration and send
// a signal saying that the test is done.
defer func() {
+ // Consider any uncaught panic a failure.
+ if err := recover(); err != nil {
+ t.failed = true
+ t.Log(err)
+ t.stack()
+ }
+
t.duration = time.Now().Sub(t.start)
t.signal <- t
}()
変更の背景
Go言語のテストフレームワークにおいて、テスト関数内でパニックが発生した場合、それまでのテスト実行が中断され、テストスイート全体が予期せぬ終了となる可能性がありました。これは、テストの信頼性を損ない、問題の根本原因を特定することを困難にしていました。
このコミットの背景には、テストの堅牢性を高め、開発者がより効率的にバグを特定できるようにするという目的があります。具体的には、テスト中に発生したパニックを捕捉し、それをテストの失敗として明確に報告することで、テスト結果の正確性を保証し、パニックの原因となったコードパスを特定するための情報(スタックトレースなど)を提供することが求められていました。これにより、テストが途中で終了することなく、パニックが発生したテストケースを特定し、その詳細なエラー情報を確認できるようになります。
前提知識の解説
Go言語におけるpanic
とrecover
Go言語には、プログラムの異常終了を扱うためのpanic
とrecover
という組み込み関数があります。
panic
:panic
関数は、現在のゴルーチン(goroutine)の実行を停止させ、遅延関数(defer
で登録された関数)を順次実行しながらスタックを巻き戻します。通常、回復不可能なエラーや、プログラムの続行が不可能になった場合に呼び出されます。panic
が捕捉されない場合、プログラム全体がクラッシュします。recover
:recover
関数は、defer
関数内で呼び出された場合にのみ有効です。recover
が呼び出されると、panic
によって停止したゴルーチンの実行を再開し、panic
に渡された引数(通常はエラー値)を返します。recover
がdefer
関数以外で呼び出された場合、またはpanic
が発生していない場合に呼び出された場合、nil
を返します。recover
を使用することで、パニックからの回復や、パニック情報をログに記録するといった処理が可能になります。
Goのtesting
パッケージ
Goの標準ライブラリであるtesting
パッケージは、ユニットテストやベンチマークテストを記述するためのフレームワークを提供します。go test
コマンドによって実行され、TestXxx
という命名規則に従う関数をテスト関数として認識します。テスト関数内では、*testing.T
型の引数を通じて、テストの失敗を報告したり、ログを出力したりするメソッド(例: t.Fail()
, t.Errorf()
, t.Log()
など)が提供されます。
スタックトレース
スタックトレース(Stack Trace)は、プログラムの実行中にエラーや例外が発生した際に、そのエラーが発生した時点での関数呼び出しの履歴を示すものです。どの関数がどの関数を呼び出し、最終的にエラーが発生したのかを追跡するのに役立ち、デバッグにおいて非常に重要な情報となります。Go言語では、runtime.Caller
やruntime.Stack
などの関数を使用してスタックトレース情報を取得できます。
技術的詳細
このコミットは、src/pkg/testing/testing.go
ファイルに以下の2つの主要な変更を加えています。
-
common
構造体にstack()
メソッドの追加: この新しいメソッドは、現在のゴルーチンのスタックトレース情報を取得し、c.Logf
を使用してテストのログに出力します。runtime.Caller(i)
を使用して呼び出し元のファイル名、行番号、プログラムカウンタ(PC)を取得し、runtime.FuncForPC(pc)
で関数名を取得しています。ループはi=2
から開始しており、これはstack()
メソッド自体の呼び出し元(tRunner
内のdefer
関数)とそのさらに呼び出し元(テスト関数)からスタックトレースを収集するためです。TODO(dsymonds): Consider hooking into runtime·traceback instead.
というコメントは、将来的にGoランタイムのより低レベルなトレースバック機能を利用する可能性を示唆しています。 -
tRunner
関数内のdefer
ブロックの変更:tRunner
関数は、個々のテスト関数を実行する役割を担っています。この関数内の既存のdefer
ブロックに、panic
を捕捉して処理するロジックが追加されました。if err := recover(); err != nil
:recover()
を呼び出すことで、テスト関数内で発生したパニックを捕捉します。パニックが発生した場合、err
にはパニックに渡された値が格納されます。t.failed = true
: パニックが捕捉された場合、該当するテスト(t
)を失敗としてマークします。これにより、テストスイートの最終結果に反映されます。t.Log(err)
: 捕捉されたパニックの値(通常はエラーメッセージ)をテストのログに出力します。t.stack()
: 新しく追加されたstack()
メソッドを呼び出し、パニック発生時のスタックトレースをログに出力します。これにより、パニックがどこで発生したかを詳細に追跡できるようになります。
これらの変更により、テスト実行中にパニックが発生しても、テストランナーがクラッシュすることなく、パニック情報とスタックトレースをログに出力し、テストを失敗として適切に処理できるようになりました。
コアとなるコードの変更箇所
変更はsrc/pkg/testing/testing.go
ファイルに集中しており、具体的には以下の2つのセクションです。
-
common
構造体へのstack()
メソッドの追加:// TODO(dsymonds): Consider hooking into runtime·traceback instead. func (c *common) stack() { for i := 2; ; i++ { // Caller we care about is the user, 2 frames up pc, file, line, ok := runtime.Caller(i) f := runtime.FuncForPC(pc) if !ok || f == nil { break } c.Logf("%s:%d (0x%x)", file, line, pc) c.Logf("\t%s", f.Name()) } }
-
tRunner
関数内のdefer
ブロックの修正:func tRunner(t *T, test *InternalTest) { // ... (既存のコード) ... defer func() { // Consider any uncaught panic a failure. if err := recover(); err != nil { t.failed = true t.Log(err) t.stack() } t.duration = time.Now().Sub(t.start) t.signal <- t }() // ... (既存のコード) ... }
コアとなるコードの解説
func (c *common) stack()
このメソッドは、*testing.T
や*testing.B
が埋め込んでいるcommon
構造体のメソッドとして定義されています。その目的は、テスト実行中にパニックが発生した際に、そのパニックがどのコードパスで発生したかを特定するためのスタックトレース情報を収集し、テストのログに出力することです。
for i := 2; ; i++
: ループはi=2
から始まります。これは、runtime.Caller(0)
がCaller
関数自身、runtime.Caller(1)
がstack()
メソッドを呼び出した関数(この場合はtRunner
内のdefer
関数)、そしてruntime.Caller(2)
がそのさらに呼び出し元、つまりパニックを発生させた可能性のあるユーザーのテストコードのフレームを指すためです。ループは、有効な呼び出し元情報が取得できなくなるまで続きます。pc, file, line, ok := runtime.Caller(i)
: 指定されたスタックフレームのプログラムカウンタ(pc
)、ファイル名(file
)、行番号(line
)を取得します。ok
は情報が正常に取得できたかを示します。f := runtime.FuncForPC(pc)
: プログラムカウンタpc
に対応するruntime.Func
オブジェクトを取得します。これにより、関数名などの情報を取得できます。if !ok || f == nil { break }
: スタックフレーム情報が取得できない、または関数情報が取得できない場合はループを終了します。c.Logf("%s:%d (0x%x)", file, line, pc)
: 取得したファイル名、行番号、プログラムカウンタをテストログに出力します。c.Logf("\t%s", f.Name())
: 取得した関数名をテストログに出力します。
このstack()
メソッドにより、パニック発生時の詳細な実行コンテキストがテスト結果に記録され、デバッグ作業が大幅に効率化されます。
tRunner
関数内のdefer
ブロック
tRunner
関数は、go test
コマンドによって各テスト関数を実行するために呼び出される内部関数です。この関数は、テスト関数の実行が完了した後に必ず実行されるdefer
関数を登録しています。このコミットでは、このdefer
関数内にパニック処理ロジックが追加されました。
if err := recover(); err != nil
: これがパニック捕捉の核心部分です。recover()
はdefer
関数内で呼び出された場合にのみ、パニックを捕捉し、そのパニックに渡された値を返します。パニックが発生していなければnil
を返します。err != nil
の条件は、パニックが実際に発生し、捕捉されたことを意味します。t.failed = true
:*testing.T
のfailed
フィールドをtrue
に設定することで、現在のテストが失敗したことをマークします。これは、go test
が最終的なテスト結果を報告する際に利用されます。t.Log(err)
: 捕捉されたパニックの値(通常はエラーメッセージや任意のオブジェクト)をテストの標準出力にログとして記録します。t.stack()
: 前述のstack()
メソッドを呼び出し、パニック発生時のスタックトレースをログに出力します。これにより、パニックがテストコードのどの部分で発生したかを正確に把握できます。
このdefer
ブロックの追加により、テスト関数内で発生した未捕捉のパニックがテストランナーをクラッシュさせることなく、テストの失敗として適切に処理され、詳細なデバッグ情報が提供されるようになりました。これは、Goのテストフレームワークの堅牢性とユーザビリティを大きく向上させる変更です。
関連リンク
- Go Code Review 5633044: https://golang.org/cl/5633044
参考にした情報源リンク
- Go言語の
panic
とrecover
に関する公式ドキュメントやチュートリアル - Go言語の
testing
パッケージに関する公式ドキュメント - Go言語の
runtime
パッケージ(特にruntime.Caller
とruntime.FuncForPC
)に関するドキュメント - Go言語の
defer
ステートメントに関するドキュメント