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

[インデックス 18335] ファイルの概要

このコミットは、Go言語の標準ライブラリであるtestingパッケージ内のtesting.goファイルに対する変更です。testingパッケージは、Goプログラムの自動テスト、ベンチマーク、ファジングをサポートするための機能を提供します。testing.goは、*testing.T(テスト)および*testing.B(ベンチマーク)型のコアロジック、テストの実行フロー、およびFailNow()SkipNow()といったテスト制御メソッドの実装を含んでいます。

コミット

testing: fix SkipNow and FailNow to avoid panic(nil) check

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/91fbf6f159a099a273e6880a5fe40351d61270b6

元コミット内容

testing: fix SkipNow and FailNow to avoid panic(nil) check

Sorry, too many windows in which to run all.bash.
Fixes build.

TBR=r
CC=golang-codereviews
https://golang.org/cl/55790043

変更の背景

このコミットは、testingパッケージにおけるSkipNow()およびFailNow()メソッドの挙動に関連するバグを修正します。これらのメソッドは、テストの実行を即座に停止し、それぞれテストをスキップまたは失敗としてマークするために使用されます。内部的には、これらのメソッドはruntime.Goexit()を呼び出して、現在のゴルーチン(テストを実行しているゴルーチン)を終了させます。

問題は、tRunner関数(個々のテストを実行する内部関数)のdeferブロックにありました。このdeferブロックは、テストがパニックを起こしたかどうかを検出し、もしパニックがnilであった場合にfmt.Errorf("test executed panic(nil)")というエラーを生成していました。しかし、runtime.Goexit()が呼び出された場合、recover()nilを返します。これは、panic(nil)が発生した場合と同じ結果になります。

したがって、SkipNow()FailNow()が呼び出された際に、tRunnerdeferブロックが誤って「panic(nil)が発生した」と解釈し、不必要なエラーを報告してしまう可能性がありました。この挙動は、テストのビルドプロセスに影響を与え、ビルドが失敗する原因となっていました。このコミットは、SkipNow()FailNow()が正常にテストを終了させた場合と、実際にpanic(nil)が発生した場合を区別できるようにすることで、この問題を解決します。

前提知識の解説

  • testingパッケージ: Go言語の標準ライブラリで、ユニットテスト、ベンチマークテスト、ファジングテストを記述するためのフレームワークを提供します。
  • *testing.T: 個々のテスト関数に渡される型で、テストのステータス(成功、失敗、スキップ)を制御したり、ログを出力したりするためのメソッドを提供します。
  • *testing.B: ベンチマーク関数に渡される型で、ベンチマークの実行を制御するためのメソッドを提供します。
  • t.FailNow(): *testing.Tのメソッドで、現在のテストを失敗としてマークし、そのテストの実行を即座に停止します。テスト関数内の後続のコードは実行されません。
  • t.SkipNow(): *testing.Tのメソッドで、現在のテストをスキップとしてマークし、そのテストの実行を即座に停止します。テスト関数内の後続のコードは実行されません。
  • runtime.Goexit(): runtimeパッケージの関数で、現在のゴルーチンを終了させます。この関数が呼び出されると、現在のゴルーチン内のすべてのdefer関数が実行された後、ゴルーチンが終了します。プログラム全体は終了せず、他のゴルーチンは引き続き実行されます。panicとは異なり、recover()では捕捉できません。
  • panic(): Go言語の組み込み関数で、現在のゴルーチンの通常の実行フローを停止させます。panicが発生すると、スタックがアンワインドされ、defer関数が実行されます。recover()関数を使ってpanicを捕捉し、プログラムのクラッシュを防ぐことができます。
  • recover(): defer関数内で呼び出される組み込み関数で、panicから回復するために使用されます。panicが発生していない場合はnilを返し、panicが発生している場合はpanicに渡された引数を返します。
  • tRunner関数: testingパッケージの内部関数で、個々のテスト関数(func(t *T))を実行するゴルーチンです。テストの開始、終了、パニックの処理、およびFailNow()SkipNow()による終了の管理を行います。

技術的詳細

このコミットの核心は、testingパッケージがFailNow()SkipNow()によってテストが終了したのか、それともpanic(nil)によってテストが終了したのかを正確に区別できるようにすることです。

従来のtRunner関数では、テストの実行が終了した際に、defer関数内でrecover()を呼び出し、その結果がnilであるかどうかをチェックしていました。

		var finished bool
		defer func() {
			t.duration = time.Now().Sub(t.start)
			// If the test panicked, print any test output before dying.
			err := recover()
			if !finished && err == nil {
				err = fmt.Errorf("test executed panic(nil)")
			}
			// ... (rest of the defer block)
		}()
		t.start = time.Now()
		test.F(t)
		finished = true

ここで、finishedというローカル変数が導入され、テスト関数test.F(t)が正常に完了した場合にtrueに設定されていました。しかし、FailNow()SkipNow()runtime.Goexit()を呼び出すと、test.F(t)の実行は途中で停止し、finished = trueの行は実行されません。このため、deferブロックが実行される際にはfinishedfalseのままであり、recover()nilを返すため、!finished && err == nilの条件が真となり、誤って"test executed panic(nil)"というエラーが生成されていました。

この修正では、common構造体にfinishedという新しいフィールドが追加されます。このフィールドは、testing.Ttesting.Bのインスタンスに紐付けられ、テストのライフサイクル全体でその状態を追跡します。

  • FailNow()SkipNow()が呼び出された際に、runtime.Goexit()を呼び出す直前にc.finished = trueを設定するように変更されました。これにより、これらのメソッドがテストを正常に終了させたことを明示的にマークします。
  • tRunner関数内のdeferブロックでは、ローカル変数finishedの代わりにt.finishedcommon構造体のフィールド)を使用するように変更されました。
  • エラーメッセージもより正確に"test executed panic(nil) or runtime.Goexit"に変更されました。これは、runtime.Goexit()による終了とpanic(nil)による終了が、recover()nilを返すという点で区別が難しいという事実を反映しています。しかし、t.finishedフラグの導入により、この区別は内部的に可能になりました。

この変更により、FailNow()SkipNow()が呼び出された場合、t.finishedtrueになるため、!t.finished && err == nilの条件は偽となり、誤ったpanic(nil)エラーの生成が回避されます。

コアとなるコードの変更箇所

src/pkg/testing/testing.go

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -143,10 +143,11 @@ var (
 // common holds the elements common between T and B and
 // captures common methods such as Errorf.
 type common struct {
-	mu      sync.RWMutex // guards output and failed
-	output  []byte       // Output generated by test or benchmark.
-	failed  bool         // Test or benchmark has failed.
-	skipped bool         // Test of benchmark has been skipped.
+	mu       sync.RWMutex // guards output and failed
+	output   []byte       // Output generated by test or benchmark.
+	failed   bool         // Test or benchmark has failed.
+	skipped  bool         // Test of benchmark has been skipped.
+	finished bool
 
 	start    time.Time // Time test or benchmark started
 	duration time.Duration
@@ -275,6 +276,7 @@ func (c *common) FailNow() {
 	// it would run on a test failure.  Because we send on c.signal during
 	// a top-of-stack deferred function now, we know that the send
 	// only happens after any other stacked defers have completed.
+	c.finished = true
 	runtime.Goexit()
 }
 
@@ -338,6 +340,7 @@ func (c *common) Skipf(format string, args ...interface{}) {
 // those other goroutines.
 func (c *common) SkipNow() {
 	c.skip()
+	c.finished = true
 	runtime.Goexit()
 }
 
@@ -376,13 +379,12 @@ 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() {
 		t.duration = time.Now().Sub(t.start)
 		// If the test panicked, print any test output before dying.
 		err := recover()
-		if !finished && err == nil {
-			err = fmt.Errorf("test executed panic(nil)")
+		if !t.finished && err == nil {
+			err = fmt.Errorf("test executed panic(nil) or runtime.Goexit")
 		}
 		if err != nil {
 			t.Fail()
@@ -394,7 +396,7 @@ func tRunner(t *T, test *InternalTest) {
 
 	t.start = time.Now()
 	test.F(t)
-	finished = true
+	t.finished = true
 }
 
 // An internal function but exported because it is cross-package; part of the implementation

コアとなるコードの解説

  1. common構造体へのfinishedフィールドの追加: common構造体は、*testing.T*testing.Bが共有する基盤となるデータとメソッドを保持します。ここにfinished boolフィールドが追加されました。これは、テストの実行が正常に完了したか、またはFailNow()/SkipNow()によって意図的に終了したかを追跡するためのフラグです。

  2. FailNow()およびSkipNow()でのc.finished = trueの設定: FailNow()SkipNow()の内部でruntime.Goexit()が呼び出される直前に、c.finished = trueが設定されるようになりました。これは、これらのメソッドがテストの実行を意図的に、かつ正常に終了させたことを示します。

  3. tRunnerdeferブロックの変更:

    • var finished boolというローカル変数の宣言が削除されました。
    • defer関数内で、!finished && err == nilの条件が!t.finished && err == nilに変更されました。これにより、ローカル変数ではなく、common構造体のfinishedフィールドが参照されるようになります。
    • test.F(t)の呼び出し後、finished = trueの行がt.finished = trueに変更されました。これは、テスト関数が正常に最後まで実行された場合に、common構造体のfinishedフィールドをtrueに設定することを意味します。
    • エラーメッセージが"test executed panic(nil)"から"test executed panic(nil) or runtime.Goexit"に変更されました。これは、recover()nilを返すケースがpanic(nil)runtime.Goexit()の両方で発生しうることをより正確に反映しています。しかし、t.finishedフラグによって、これらのケースは内部的に区別されます。

これらの変更により、FailNow()SkipNow()が呼び出されてruntime.Goexit()によってテストが終了した場合、c.finishedtrueに設定されているため、tRunnerdeferブロックは!t.finishedの条件が偽となり、誤ってpanic(nil)エラーを報告することがなくなります。これにより、テストのビルドが正しく行われるようになります。

関連リンク

参考にした情報源リンク