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

[インデックス 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言語におけるpanicrecover

Go言語には、プログラムの異常終了を扱うためのpanicrecoverという組み込み関数があります。

  • panic: panic関数は、現在のゴルーチン(goroutine)の実行を停止させ、遅延関数(deferで登録された関数)を順次実行しながらスタックを巻き戻します。通常、回復不可能なエラーや、プログラムの続行が不可能になった場合に呼び出されます。panicが捕捉されない場合、プログラム全体がクラッシュします。
  • recover: recover関数は、defer関数内で呼び出された場合にのみ有効です。recoverが呼び出されると、panicによって停止したゴルーチンの実行を再開し、panicに渡された引数(通常はエラー値)を返します。recoverdefer関数以外で呼び出された場合、またはpanicが発生していない場合に呼び出された場合、nilを返します。recoverを使用することで、パニックからの回復や、パニック情報をログに記録するといった処理が可能になります。

Goのtestingパッケージ

Goの標準ライブラリであるtestingパッケージは、ユニットテストやベンチマークテストを記述するためのフレームワークを提供します。go testコマンドによって実行され、TestXxxという命名規則に従う関数をテスト関数として認識します。テスト関数内では、*testing.T型の引数を通じて、テストの失敗を報告したり、ログを出力したりするメソッド(例: t.Fail(), t.Errorf(), t.Log()など)が提供されます。

スタックトレース

スタックトレース(Stack Trace)は、プログラムの実行中にエラーや例外が発生した際に、そのエラーが発生した時点での関数呼び出しの履歴を示すものです。どの関数がどの関数を呼び出し、最終的にエラーが発生したのかを追跡するのに役立ち、デバッグにおいて非常に重要な情報となります。Go言語では、runtime.Callerruntime.Stackなどの関数を使用してスタックトレース情報を取得できます。

技術的詳細

このコミットは、src/pkg/testing/testing.goファイルに以下の2つの主要な変更を加えています。

  1. 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ランタイムのより低レベルなトレースバック機能を利用する可能性を示唆しています。

  2. 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つのセクションです。

  1. 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())
    	}
    }
    
  2. 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.Tfailedフィールドをtrueに設定することで、現在のテストが失敗したことをマークします。これは、go testが最終的なテスト結果を報告する際に利用されます。
  • t.Log(err): 捕捉されたパニックの値(通常はエラーメッセージや任意のオブジェクト)をテストの標準出力にログとして記録します。
  • t.stack(): 前述のstack()メソッドを呼び出し、パニック発生時のスタックトレースをログに出力します。これにより、パニックがテストコードのどの部分で発生したかを正確に把握できます。

このdeferブロックの追加により、テスト関数内で発生した未捕捉のパニックがテストランナーをクラッシュさせることなく、テストの失敗として適切に処理され、詳細なデバッグ情報が提供されるようになりました。これは、Goのテストフレームワークの堅牢性とユーザビリティを大きく向上させる変更です。

関連リンク

参考にした情報源リンク

  • Go言語のpanicrecoverに関する公式ドキュメントやチュートリアル
  • Go言語のtestingパッケージに関する公式ドキュメント
  • Go言語のruntimeパッケージ(特にruntime.Callerruntime.FuncForPC)に関するドキュメント
  • Go言語のdeferステートメントに関するドキュメント