[インデックス 11127] ファイルの概要
このコミットは、Go言語のtesting
パッケージにおけるdefer
関数の実行順序に関する競合状態(defer race)を修正するものです。具体的には、t.Fatal()
が呼び出された際に、テストが終了する前にdefer
されたクリーンアップ関数が確実に実行されるように、テストランナー(tRunner
)のロジックが変更されています。これにより、一時ファイルの削除やファイルシステムのアンマウントといった重要なクリーンアップ処理が、テストの失敗時にも保証されるようになります。
コミット
commit 4953b87296f53c5e0c7c62a775f1c088d4212902
Author: Russ Cox <rsc@golang.org>
Date: Thu Jan 12 10:18:12 2012 -0800
testing: fix defer race
In a test that does
func TestFoo(t *testing.T) {
defer cleanup()
t.Fatal("oops")
}
it can be important that cleanup run as the test fails.
The old code did this in Fatal:
t.signal <- t
runtime.Goexit()
The runtime.Goexit would run the deferred cleanup
but the send on t.signal would cause the main test loop
to move on and possibly even exit the program before
the runtime.Goexit got a chance to run.
This CL changes tRunner (the top stack frame of a test
goroutine) to send on t.signal as part of a function
deferred by the top stack frame. This delays the send
on t.signal until after runtime.Goexit has run functions
deferred by the test itself.
For the above TestFoo, this CL guarantees that cleanup
will run before the test binary exits.
This is particularly important when cleanup is doing
externally visible work, like removing temporary files
or unmounting file systems.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5532078
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4953b87296f53c5e0c7c62a775f1c088d4212902
元コミット内容
testing: fix defer race
In a test that does
func TestFoo(t *testing.T) {
defer cleanup()
t.Fatal("oops")
}
it can be important that cleanup run as the test fails.
The old code did this in Fatal:
t.signal <- t
runtime.Goexit()
The runtime.Goexit would run the deferred cleanup
but the send on t.signal would cause the main test loop
to move on and possibly even exit the program before
the runtime.Goexit got a chance to run.
This CL changes tRunner (the top stack frame of a test
goroutine) to send on t.signal as part of a function
deferred by the top stack frame. This delays the send
on t.signal until after runtime.Goexit has run functions
deferred by the test itself.
For the above TestFoo, this CL guarantees that cleanup
will run before the test binary exits.
This is particularly important when cleanup is doing
externally visible work, like removing temporary files
or unmounting file systems.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5532078
変更の背景
Goのtesting
パッケージにおいて、テスト関数内でdefer
キーワードを使って遅延実行される関数(例えば、テスト後に一時ファイルを削除するcleanup
関数など)を登録し、かつテストがt.Fatal()
やt.FailNow()
によって途中で終了する場合に問題が発生していました。
従来のt.Fatal()
の実装では、まずテストの終了を通知するシグナルをチャネル(t.signal
)に送信し、その後にruntime.Goexit()
を呼び出して現在のゴルーチンを終了させていました。runtime.Goexit()
は、現在のゴルーチンが終了する際に、そのゴルーチン内でdefer
された関数をすべて実行するという特性があります。
しかし、t.signal
への送信がruntime.Goexit()
の呼び出しよりも前に行われるため、メインのテストループがテストの終了を検知し、次のテストに進んだり、場合によってはプログラム全体が終了したりする可能性がありました。この「テストの終了通知」と「defer
された関数の実行」の間に競合状態が生じ、runtime.Goexit()
がdefer
された関数を実行する機会を得る前に、テストバイナリが終了してしまうことがありました。
この問題は、特に一時ファイルの削除やファイルシステムのアンマウントなど、外部に影響を与えるクリーンアップ処理がテストの失敗時に確実に実行されないという深刻な結果を招く可能性がありました。このコミットは、この競合状態を解消し、defer
されたクリーンアップ関数がテストの失敗時にも確実に実行されるようにするために導入されました。
前提知識の解説
Go言語のdefer
キーワード
defer
キーワードは、Go言語において関数の実行を遅延させるために使用されます。defer
に続く関数呼び出しは、その関数がリターンする直前(パニックが発生した場合も含む)に実行されることが保証されます。これは、リソースの解放(ファイルのクローズ、ロックの解除など)やクリーンアップ処理を確実に行うために非常に便利です。defer
された関数はLIFO(Last-In, First-Out)の順序で実行されます。
runtime.Goexit()
runtime.Goexit()
は、現在のゴルーチンを終了させるためのGoの組み込み関数です。この関数が呼び出されると、現在のゴルーチンは直ちに実行を停止し、そのゴルーチン内でdefer
されたすべての関数が実行されます。その後、ゴルーチンは終了します。runtime.Goexit()
は、panic
とは異なり、呼び出し元のスタックをアンワインドせず、呼び出し元に制御を戻しません。
testing
パッケージとt.Fatal()
/ t.FailNow()
Goの標準ライブラリであるtesting
パッケージは、ユニットテストやベンチマークテストを記述するためのフレームワークを提供します。
*testing.T
は個々のテストのコンテキストを表します。t.Fatal(args ...interface{})
またはt.Fatalf(format string, args ...interface{})
は、テストを失敗としてマークし、現在のテストゴルーチンを直ちに終了させます。この関数が呼び出されると、テスト関数内のそれ以降のコードは実行されません。内部的にはt.FailNow()
を呼び出します。t.FailNow()
は、テストを失敗としてマークし、現在のテストゴルーチンをruntime.Goexit()
を呼び出すことで終了させます。
tRunner
tRunner
は、testing
パッケージ内部で各テスト関数を実行するために使用される関数です。各テスト関数は、tRunner
によって新しいゴルーチンで起動されます。tRunner
は、テストの開始時刻の記録、テスト関数の実行、テストの終了時刻の記録、そしてテスト結果をメインのテストループに通知する役割を担っています。
技術的詳細
このコミットが解決しようとしている問題は、t.Fatal()
(またはt.FailNow()
)が呼び出された際のdefer
された関数の実行と、テスト終了のシグナル送信のタイミングに関するものです。
旧来の動作:
- テスト関数内で
t.Fatal()
が呼び出される。 t.Fatal()
内部で、まずc.signal <- c.self
(c
は*testing.T
または*testing.B
の共通構造体)によって、テストが終了したことをメインのテストループに通知するシグナルが送信される。- 次に
runtime.Goexit()
が呼び出され、現在のテストゴルーチンが終了し、そのゴルーチン内でdefer
された関数が実行される。
この順序では、シグナルが送信された後、メインのテストループがテストゴルーチンの終了を待たずに次の処理(例えば、次のテストの開始やプログラムの終了)に進んでしまう可能性がありました。その結果、runtime.Goexit()
がdefer
されたクリーンアップ関数を実行する前に、テストバイナリ自体が終了してしまうという「競合状態」が発生していました。
新しい動作:
このコミットでは、t.signal
へのシグナル送信のタイミングが変更されました。
t.Fatal()
からはc.signal <- c.self
の行が削除され、純粋にruntime.Goexit()
を呼び出すだけになりました。- 代わりに、
tRunner
関数(各テストゴルーチンの最上位スタックフレーム)に、新しいdefer
関数が追加されました。このdefer
関数は、tRunner
ゴルーチンが終了する際に、テストの実行時間を記録し、t.signal <- t
によってテスト終了のシグナルを送信します。
この変更により、t.Fatal()
が呼び出された場合でも、runtime.Goexit()
がまずdefer
されたすべての関数(テスト関数内でdefer
されたクリーンアップ関数を含む)を実行します。そして、そのゴルーチンが完全に終了する直前に、tRunner
に新しく追加されたdefer
関数が実行され、そこでt.signal
へのシグナル送信が行われます。
これにより、t.signal
へのシグナル送信は、テストゴルーチン内のすべてのdefer
関数が実行された後にのみ行われることが保証されます。結果として、メインのテストループがテストの終了を検知する前に、重要なクリーンアップ処理が確実に完了するようになります。
コアとなるコードの変更箇所
src/pkg/testing/benchmark.go
--- a/src/pkg/testing/benchmark.go
+++ b/src/pkg/testing/benchmark.go
@@ -142,6 +142,13 @@ func (b *B) run() BenchmarkResult {
func (b *B) launch() {
// Run the benchmark for a single iteration in case it's expensive.
n := 1
+
+ // Signal that we're done whether we return normally
+ // or by FailNow's runtime.Goexit.
+ defer func() {
+ b.signal <- b
+ }()
+
b.runN(n)
// Run the benchmark for at least the specified amount of time.
d := time.Duration(*benchTime * float64(time.Second))
@@ -162,7 +169,6 @@ func (b *B) launch() {
b.runN(n)
}
b.result = BenchmarkResult{b.N, b.duration, b.bytes}
- b.signal <- b
}
src/pkg/testing/testing.go
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -136,9 +136,27 @@ func (c *common) Failed() bool { return c.failed }
// FailNow marks the function as having failed and stops its execution.
// Execution will continue at the next Test.
func (c *common) FailNow() {
- c.duration = time.Now().Sub(c.start)
c.Fail()
- c.signal <- c.self
+
+ // Calling runtime.Goexit will exit the goroutine, which
+ // will run the deferred functions in this goroutine,
+ // which will eventually run the deferred lines in tRunner,
+ // which will signal to the test loop that this test is done.
+ //
+ // A previous version of this code said:
+ //
+ // c.duration = ...
+ // c.signal <- c.self
+ // runtime.Goexit()
+ //
+ // This previous version duplicated code (those lines are in
+ // tRunner no matter what), but worse the goroutine teardown
+ // implicit in runtime.Goexit was not guaranteed to complete
+ // before the test exited. If a test deferred an important cleanup
+ // function (like removing temporary files), there was no guarantee
+ // 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.
runtime.Goexit()
}
@@ -195,9 +213,17 @@ type InternalTest struct {
func tRunner(t *T, test *InternalTest) {
t.start = time.Now()
+
+ // When this goroutine is done, either because test.F(t)
+ // 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.
+ defer func() {
+ t.duration = time.Now().Sub(t.start)
+ t.signal <- t
+ }()
+
test.F(t)
- t.duration = time.Now().Sub(t.start)
- t.signal <- t
}
コアとなるコードの解説
src/pkg/testing/testing.go
の変更
-
FailNow()
関数の変更:- 以前は
c.duration = time.Now().Sub(c.start)
とc.signal <- c.self
がruntime.Goexit()
の前に実行されていました。 - これらの行が削除され、
FailNow()
はc.Fail()
を呼び出した後、直接runtime.Goexit()
を呼び出すだけになりました。 - これにより、テストの失敗シグナルが送信される前に、
runtime.Goexit()
によってdefer
された関数が確実に実行されるようになります。コメントにもあるように、以前のバージョンではruntime.Goexit
によるゴルーチンのティアダウンがテスト終了前に完了することが保証されていませんでした。
- 以前は
-
tRunner
関数の変更:tRunner
は、各テスト関数が実行されるゴルーチンの最上位の関数です。- 新しい
defer
関数が追加されました。このdefer
関数は、tRunner
ゴルーチンが正常に終了する場合でも、runtime.Goexit()
によって終了する場合でも、必ず実行されます。 - この
defer
関数内で、テストの実行時間(t.duration
)が計算され、t.signal <- t
によってテスト終了のシグナルが送信されます。 - これにより、
t.signal
へのシグナル送信は、tRunner
ゴルーチンが完全に終了する直前、つまりテスト関数内でdefer
されたすべての関数が実行された後にのみ行われることが保証されます。
src/pkg/testing/benchmark.go
の変更
launch()
関数の変更:*testing.B
(ベンチマークテスト)のlaunch()
関数も同様の競合状態を抱えていたため、tRunner
と同様の修正が適用されました。- 以前は
b.signal <- b
が関数の最後に直接呼び出されていました。 - この行が削除され、代わりに新しい
defer
関数が追加されました。このdefer
関数内でb.signal <- b
が実行されます。 - これにより、ベンチマークテストが正常に終了する場合でも、
FailNow()
によって終了する場合でも、defer
されたクリーンアップ処理が完了した後にのみシグナルが送信されるようになります。
これらの変更により、testing
パッケージは、テスト関数内でdefer
されたクリーンアップ処理が、テストが成功したか失敗したかにかかわらず、常に確実に実行されることを保証するようになりました。これは、テストの信頼性と堅牢性を高める上で非常に重要な改善です。
関連リンク
- Go CL 5532078: https://golang.org/cl/5532078
参考にした情報源リンク
- Go言語の公式ドキュメント(
defer
、runtime.Goexit
、testing
パッケージに関する情報) - コミットメッセージとコードの差分