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

[インデックス 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

変更の背景

この変更の主な背景は以下の通りです。

  1. 出力の簡潔化: 以前のtestingパッケージは、テスト中にパニックが発生した場合、runtime/debug.Stack()を使用して詳細なスタックトレースを独自に取得し、t.Logfで出力していました。しかし、この出力にはテストのデバッグには直接関係のない情報(例: testingパッケージ内部の_func_003のような関数呼び出し)が含まれており、冗長でした。このコミットは、ランタイムにパニック処理を任せることで、より簡潔で関連性の高いスタックトレースを出力することを目指しています。
  2. 依存関係の削減: 以前の実装では、testingパッケージがruntime/debugパッケージに依存していました。この依存関係を解消することで、testingパッケージの独立性を高め、ビルド時の複雑さを軽減します。
  3. テストバイナリの停止: テスト中にパニックが発生した場合、以前はtestingパッケージがrecoverしてテストの実行を継続しようとしていました。しかし、パニックは通常、回復不能なエラーを示すため、テストがパニックした時点でテストバイナリ全体が停止する方が、問題の早期発見とデバッグに役立つという判断がなされました。コミットメッセージにある「That's a feature, not a bug.」という記述は、この挙動が意図されたものであることを強調しています。
  4. 出力順序の保証: テストがパニックする前にt.Logなどで出力された情報が、パニックのスタックトレースよりも前に表示されるように、出力順序が保証されるようになりました。これにより、パニックに至るまでのテストの挙動をより正確に把握できるようになります。

前提知識の解説

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

1. Go言語のパニック(Panic)と回復(Recover)

  • パニック(Panic): Go言語におけるパニックは、プログラムの実行中に発生する回復不能なエラーを示すメカニズムです。例えば、nilポインタのデリファレンス、配列の範囲外アクセス、ゼロ除算などがパニックを引き起こす可能性があります。パニックが発生すると、通常のプログラムフローは中断され、現在のゴルーチン(goroutine)の関数呼び出しスタックを遡りながら、defer関数が実行されます。最終的に、recoverが呼び出されない限り、プログラム全体が異常終了します。
  • 回復(Recover): recoverは、defer関数内で呼び出される組み込み関数です。recoverが呼び出されると、現在のゴルーチンで発生したパニックを捕捉し、パニックの引数(通常はエラー値)を返します。recoverがパニックを捕捉すると、そのゴルーチンの実行は正常な状態に戻り、defer関数の残りの処理が実行された後、defer関数が定義されている関数の次のステートメントから実行が再開されます。recoverdefer関数内でしか意味を持ちません。

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
}()

この変更のポイントは以下の通りです。

  1. runtime/debugパッケージの依存関係の削除: testing.goからimport "runtime/debug"が削除されました。これにより、testingパッケージはスタックトレースの取得のためにこのパッケージに依存する必要がなくなりました。
  2. パニック処理の委譲: testingパッケージがパニックを捕捉した後、それを再発生させることで、Goランタイムのデフォルトのパニック処理メカニズムに委ねています。ランタイムは、より簡潔で関連性の高いスタックトレースを自動的に出力します。これにより、以前の冗長なtesting.go内部の呼び出しスタックが削除され、ユーザーにとってより分かりやすい出力が得られます。
  3. テストバイナリの停止: パニックがランタイムに委ねられるため、テスト中にパニックが発生すると、テストバイナリ全体が停止するようになります。これは、テストが失敗した際に即座に問題を特定し、デバッグを開始するための意図的な挙動です。
  4. 出力順序の保証: 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.failedtrueに設定していましたが、テストバイナリが停止するようになったため、このフラグを設定する必要がなくなりました。
    • 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言語の公式ドキュメントおよびtestingパッケージのソースコード
  • Go言語におけるpanicrecoverの一般的な挙動に関する知識
  • コミットメッセージに記載されている「before」と「after」の出力例
  • Go言語のruntime/debugパッケージに関する一般的な知識