[インデックス 11875] ファイルの概要
このコミットは、Go言語の標準ライブラリであるtesting
パッケージにおけるパニック(panic)処理の挙動を変更するものです。具体的には、テスト中に発生したパニックをtesting
パッケージ自身がrecover
して詳細なスタックトレースを出力するのではなく、パニックを再発生させてGoランタイムに処理を委ねるように変更しています。これにより、出力される情報がより簡潔になり、testing
パッケージがruntime/debug
パッケージに依存するのを避けることができます。また、テストがパニックした場合にテストバイナリが停止するようになり、これは意図された挙動(「機能でありバグではない」)とされています。
コミット
- コミットハッシュ:
bf2838334c76312bb65c95e6cbdfa1d40c8e4074
- Author: Rob Pike r@golang.org
- Date: Tue Feb 14 14:53:30 2012 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bf2838334c76312bb65c95e6cbdfa1d40c8e4074
元コミット内容
testing: let runtime catch the panic.
It's not as pretty, but it deletes some irrelevant information from the
printout and avoids a dependency.
It also means the test binary will stop if a test panics. That's a feature,
not a bug.
Any output printed by the test appears before the panic traceback.
before:
--- FAIL: TestPanic (0.00 seconds)
fmt_test.go:19: HI
testing.go:257: runtime error: index out of range
/Users/r/go/src/pkg/testing/testing.go:257 (0x23998)
_func_003: t.Logf("%s\n%s", err, debug.Stack())
/Users/r/go/src/pkg/runtime/proc.c:1388 (0x10d2d)
panic: reflect·call(d->fn, d->args, d->siz);
/Users/r/go/src/pkg/runtime/runtime.c:128 (0x119b0)
panicstring: runtime·panic(err);
/Users/r/go/src/pkg/runtime/runtime.c:85 (0x11857)
panicindex: runtime·panicstring("index out of range");
/Users/r/go/src/pkg/fmt/fmt_test.go:21 (0x23d72)
TestPanic: a[10]=1
/Users/r/go/src/pkg/testing/testing.go:264 (0x21b75)
tRunner: test.F(t)
/Users/r/go/src/pkg/runtime/proc.c:258 (0xee9e)
goexit: runtime·goexit(void)
FAIL
after:
--- FAIL: TestPanic (0.00 seconds)
fmt_test.go:19: HI
panic: runtime error: index out of range [recovered]
panic: (*testing.T) (0xec3b0,0xf8400001c0)
goroutine 2 [running]:
testing._func_003(0x21f5fa8, 0x21f5100, 0x21f5fb8, 0x21f5e88)
/Users/r/go/src/pkg/testing/testing.go:259 +0x108
----- stack segment boundary -----
fmt_test.TestPanic(0xf8400001c0, 0x27603728)
/Users/r/go/src/pkg/fmt/fmt_test.go:21 +0x6b
testing.tRunner(0xf8400001c0, 0x18edb8, 0x0, 0x0)
/Users/r/go/src/pkg/testing/testing.go:264 +0x6f
created by testing.RunTests
/Users/r/go/src/pkg/testing/testing.go:343 +0x76e
goroutine 1 [chan receive]:
testing.RunTests(0x2000, 0x18edb8, 0x2400000024, 0x100000001, 0x200000001, ...)
/Users/r/go/src/pkg/testing/testing.go:344 +0x791
testing.Main(0x2000, 0x18edb8, 0x2400000024, 0x188a58, 0x800000008, ...)\n /src/pkg/testing/testing.go:275 +0x62
main.main()
/var/folders/++/+++Fn+++6+0++4RjPqRgNE++2Qk/-Tmp-/go-build743922747/fmt/_test/_testmain.go:129 +0x91
exit status 2
R=rsc, dsymonds
CC=golang-dev
https://golang.org/cl/5658048
変更の背景
この変更の主な背景は以下の通りです。
- 出力の簡潔化: 以前の
testing
パッケージは、テスト中にパニックが発生した場合、runtime/debug.Stack()
を使用して詳細なスタックトレースを独自に取得し、t.Logf
で出力していました。しかし、この出力にはテストのデバッグには直接関係のない情報(例:testing
パッケージ内部の_func_003
のような関数呼び出し)が含まれており、冗長でした。このコミットは、ランタイムにパニック処理を任せることで、より簡潔で関連性の高いスタックトレースを出力することを目指しています。 - 依存関係の削減: 以前の実装では、
testing
パッケージがruntime/debug
パッケージに依存していました。この依存関係を解消することで、testing
パッケージの独立性を高め、ビルド時の複雑さを軽減します。 - テストバイナリの停止: テスト中にパニックが発生した場合、以前は
testing
パッケージがrecover
してテストの実行を継続しようとしていました。しかし、パニックは通常、回復不能なエラーを示すため、テストがパニックした時点でテストバイナリ全体が停止する方が、問題の早期発見とデバッグに役立つという判断がなされました。コミットメッセージにある「That's a feature, not a bug.」という記述は、この挙動が意図されたものであることを強調しています。 - 出力順序の保証: テストがパニックする前に
t.Log
などで出力された情報が、パニックのスタックトレースよりも前に表示されるように、出力順序が保証されるようになりました。これにより、パニックに至るまでのテストの挙動をより正確に把握できるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とtesting
パッケージの基本的な知識が必要です。
1. Go言語のパニック(Panic)と回復(Recover)
- パニック(Panic): Go言語におけるパニックは、プログラムの実行中に発生する回復不能なエラーを示すメカニズムです。例えば、nilポインタのデリファレンス、配列の範囲外アクセス、ゼロ除算などがパニックを引き起こす可能性があります。パニックが発生すると、通常のプログラムフローは中断され、現在のゴルーチン(goroutine)の関数呼び出しスタックを遡りながら、
defer
関数が実行されます。最終的に、recover
が呼び出されない限り、プログラム全体が異常終了します。 - 回復(Recover):
recover
は、defer
関数内で呼び出される組み込み関数です。recover
が呼び出されると、現在のゴルーチンで発生したパニックを捕捉し、パニックの引数(通常はエラー値)を返します。recover
がパニックを捕捉すると、そのゴルーチンの実行は正常な状態に戻り、defer
関数の残りの処理が実行された後、defer
関数が定義されている関数の次のステートメントから実行が再開されます。recover
はdefer
関数内でしか意味を持ちません。
2. Go言語のtesting
パッケージ
Go言語のtesting
パッケージは、ユニットテスト、ベンチマークテスト、および例(Example)を記述するためのフレームワークを提供します。
- テスト関数:
TestXxx
という命名規則に従う関数がテスト関数として認識されます。これらの関数は*testing.T
型の引数を取ります。 **testing.T**
: テストの実行中に状態を管理し、テストの失敗を報告したり、ログを出力したりするためのメソッドを提供します。t.Fail()
: テストを失敗としてマークしますが、テストの実行は継続します。t.FailNow()
: テストを失敗としてマークし、現在のテストゴルーチンを停止します。t.Logf()
: テストの実行中にログメッセージを出力します。t.Panic()
: テスト中に意図的にパニックを発生させます。
tRunner
:testing
パッケージの内部関数で、個々のテスト関数を実行するゴルーチンを管理します。この関数内で、テスト関数の実行中に発生したパニックを捕捉し、処理するロジックが含まれています。
3. スタックトレース(Stack Trace)
スタックトレースは、プログラムの実行中にエラーや例外(Goではパニック)が発生した時点での関数呼び出しの履歴を示すものです。どの関数がどの関数を呼び出し、最終的にエラーが発生したのかを追跡するのに役立ちます。Go言語では、パニックが発生するとデフォルトでスタックトレースが出力されます。runtime/debug.Stack()
関数は、現在のゴルーチンのスタックトレースをバイトスライスとして返します。
技術的詳細
このコミットは、src/pkg/testing/testing.go
内のtRunner
関数におけるパニック処理のロジックを根本的に変更しています。
変更前:
tRunner
関数内のdefer
ブロックでは、テスト関数がパニックした場合にrecover()
を呼び出してパニックを捕捉していました。捕捉したエラー情報とruntime/debug.Stack()
で取得したスタックトレースをt.Logf()
で出力し、t.failed = true
を設定してテストを失敗としてマークしていました。このアプローチは、テストがパニックしてもテストバイナリ全体の実行を継続させようとするものでした。
// src/pkg/testing/testing.go (変更前)
defer func() {
// Log and recover from panic instead of aborting binary.
if err := recover(); err != nil {
t.failed = true
t.Logf("%s\n%s", err, debug.Stack()) // ここでスタックトレースを独自に出力
}
t.duration = time.Now().Sub(t.start)
t.signal <- t
}()
変更後:
変更後もdefer
ブロック内でrecover()
を呼び出してパニックを捕捉しますが、捕捉したパニックをt.Logf()
で出力する代わりに、まずt.report()
を呼び出し、その後捕捉したパニックをpanic(err)
で再発生させています。
// src/pkg/testing/testing.go (変更後)
defer func() {
t.duration = time.Now().Sub(t.start) // 実行時間を先に記録
// If the test panicked, print any test output before dying.
if err := recover(); err != nil {
t.report() // テストの出力(t.Logfなど)を先に報告
panic(err) // パニックを再発生させ、ランタイムに処理を委ねる
}
t.signal <- t
}()
この変更のポイントは以下の通りです。
runtime/debug
パッケージの依存関係の削除:testing.go
からimport "runtime/debug"
が削除されました。これにより、testing
パッケージはスタックトレースの取得のためにこのパッケージに依存する必要がなくなりました。- パニック処理の委譲:
testing
パッケージがパニックを捕捉した後、それを再発生させることで、Goランタイムのデフォルトのパニック処理メカニズムに委ねています。ランタイムは、より簡潔で関連性の高いスタックトレースを自動的に出力します。これにより、以前の冗長なtesting.go
内部の呼び出しスタックが削除され、ユーザーにとってより分かりやすい出力が得られます。 - テストバイナリの停止: パニックがランタイムに委ねられるため、テスト中にパニックが発生すると、テストバイナリ全体が停止するようになります。これは、テストが失敗した際に即座に問題を特定し、デバッグを開始するための意図的な挙動です。
- 出力順序の保証:
panic(err)
の前にt.report()
が呼び出されることで、テスト関数内でt.Logf
などによって出力されたメッセージが、パニックのスタックトレースよりも前に確実に出力されるようになります。これにより、パニックに至るまでのテストの挙動に関するコンテキストが失われることがありません。
また、src/pkg/runtime/debug/stack_test.go
では、パッケージ名がdebug_test
からdebug
に変更されています。これは、このテストファイルがruntime/debug
パッケージの一部として扱われるようにするための変更であり、テストの構造に関するものです。
コアとなるコードの変更箇所
diff --git a/src/pkg/runtime/debug/stack_test.go b/src/pkg/runtime/debug/stack_test.go
index f1a307579c..cf4bd0238e 100644
--- a/src/pkg/runtime/debug/stack_test.go
+++ b/src/pkg/runtime/debug/stack_test.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package debug_test
+package debug
import (
. "runtime/debug"
diff --git a/src/pkg/testing/testing.go b/src/pkg/testing/testing.go
index b60d5c1b0d..bbacf8ba50 100644
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -71,7 +71,6 @@ import (
"fmt"
"os"
"runtime"
- "runtime/debug"
"runtime/pprof"
"strconv"
"strings"
@@ -248,13 +247,12 @@ func tRunner(t *T, test *InternalTest) {
// a call to runtime.Goexit, record the duration and send
// a signal saying that the test is done.\n defer func() {
-\t\t// Log and recover from panic instead of aborting binary.
+\t\tt.duration = time.Now().Sub(t.start)
+\t\t// If the test panicked, print any test output before dying.
\t\tif err := recover(); err != nil {\n-\t\t\tt.failed = true
-\t\t\tt.Logf("%s\\n%s", err, debug.Stack())
+\t\t\tt.report()
+\t\t\tpanic(err)
\t\t}
-\n-\t\tt.duration = time.Now().Sub(t.start)
\t\tt.signal <- t
\t}()
コアとなるコードの解説
src/pkg/runtime/debug/stack_test.go
--- a/src/pkg/runtime/debug/stack_test.go
+++ b/src/pkg/runtime/debug/stack_test.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package debug_test
+package debug
この変更は、stack_test.go
ファイルのパッケージ宣言をdebug_test
からdebug
に変更しています。これは、このテストファイルがruntime/debug
パッケージの内部テストとして機能するようにするためです。これにより、テストがruntime/debug
パッケージの非公開の要素にアクセスできるようになる可能性があります。
src/pkg/testing/testing.go
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -71,7 +71,6 @@ import (
"fmt"
"os"
"runtime"
- "runtime/debug" // この行が削除された
"runtime/pprof"
"strconv"
"strings"
この部分では、runtime/debug
パッケージのインポートが削除されています。これは、testing
パッケージがパニック時のスタックトレースの取得をruntime/debug.Stack()
に依存しなくなったことを意味します。これにより、testing
パッケージの依存関係が減少し、より軽量になります。
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -248,13 +247,12 @@ func tRunner(t *T, test *InternalTest) {
// a call to runtime.Goexit, record the duration and send
// a signal saying that the test is done.\n defer func() {
-\t\t// Log and recover from panic instead of aborting binary.
+\t\tt.duration = time.Now().Sub(t.start) // 実行時間を先に記録する行が移動
+\t\t// If the test panicked, print any test output before dying.
\t\tif err := recover(); err != nil {\n-\t\t\tt.failed = true // この行が削除された
-\t\t\tt.Logf("%s\\n%s", err, debug.Stack()) // この行が変更された
+\t\t\tt.report() // 新しく追加された行
+\t\t\tpanic(err) // この行が変更された
\t\t}
-\n-\t\tt.duration = time.Now().Sub(t.start) // この行が移動した
\t\tt.signal <- t
\t}()
このdefer
ブロックは、tRunner
関数が終了する際に実行されるクリーンアップ処理を定義しています。
t.duration = time.Now().Sub(t.start)
の移動: 以前はrecover
ブロックの後に実行されていましたが、変更後はdefer
ブロックの冒頭に移動しました。これにより、パニックが発生してpanic(err)
が呼び出される前にテストの実行時間が正確に記録されるようになります。- コメントの変更: 「Log and recover from panic instead of aborting binary.」から「If the test panicked, print any test output before dying.」に変更され、新しい挙動(パニック時にテストバイナリが停止すること)が明確に示されています。
if err := recover(); err != nil
ブロック内の変更:t.failed = true
の削除: 以前はパニックが発生した場合にt.failed
をtrue
に設定していましたが、テストバイナリが停止するようになったため、このフラグを設定する必要がなくなりました。t.Logf("%s\n%s", err, debug.Stack())
の削除:runtime/debug.Stack()
を使用してスタックトレースを独自にログ出力する処理が削除されました。t.report()
の追加:t.report()
は、テストの実行中にt.Logf
などで出力されたすべてのログメッセージを、パニックのスタックトレースが出力される前に確実に表示するためのものです。これにより、パニックに至るまでのテストの挙動に関する重要な情報が失われることがありません。panic(err)
の追加:recover()
で捕捉したパニックを、panic(err)
を呼び出すことで再発生させています。これにより、パニック処理がGoランタイムのデフォルトのメカニズムに委ねられます。ランタイムは、より簡潔で関連性の高いスタックトレースを出力し、テストバイナリ全体を停止させます。
これらの変更により、Goのテストにおけるパニック処理は、よりシンプルで、デバッグに役立つ情報を提供し、テストの失敗をより明確に通知するようになりました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
testing
パッケージのドキュメント: https://pkg.go.dev/testingpanic
とrecover
に関するGoブログ記事 (例: Defer, Panic, and Recover): https://go.dev/blog/defer-panic-and-recover
参考にした情報源リンク
- Go言語の公式ドキュメントおよび
testing
パッケージのソースコード - Go言語における
panic
とrecover
の一般的な挙動に関する知識 - コミットメッセージに記載されている「before」と「after」の出力例
- Go言語の
runtime/debug
パッケージに関する一般的な知識