[インデックス 15087] ファイルの概要
このコミットは、Go言語の標準ライブラリ testing
パッケージにおける FailNow
および SkipNow
関数の利用に関する重要なドキュメントの追加と修正を行っています。具体的には、これらの関数がテストまたはベンチマークを実行しているゴルーチンからのみ呼び出されるべきであり、テスト中に作成された他のゴルーチンからは呼び出すべきではないという制約を明記しています。この制約は、FailNow
や SkipNow
が呼び出し元のゴルーチンの実行を停止するものの、他のゴルーチンには影響を与えないという動作特性に起因します。
コミット
commit 2cb8dcea5c18730425c0f7ceb40c56a4c15f0d5e
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 1 21:01:32 2013 -0500
testing: SkipNow, FailNow must be called from test goroutine
Impossible for us to check (without sleazily reaching into the
runtime) but at least document it.
Fixes #3800.
R=golang-dev, bradfitz, dave
CC=golang-dev
https://golang.org/cl/7268043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2cb8dcea5c18730425c0f7ceb40c56a4c15f0d5e
元コミット内容
このコミットの元のメッセージは以下の通りです。
testing: SkipNow, FailNow must be called from test goroutine
Impossible for us to check (without sleazily reaching into the
runtime) but at least document it.
Fixes #3800.
R=golang-dev, bradfitz, dave
CC=golang-dev
https://golang.org/cl/7268043
変更の背景
この変更の背景には、Goのテストフレームワークにおける FailNow
および SkipNow
関数の誤用、またはその動作に関する誤解があったと考えられます。これらの関数は、テストの実行を即座に停止させるために設計されていますが、Goの並行処理モデル(ゴルーチン)の特性上、どのゴルーチンから呼び出されるかが重要になります。
具体的には、FailNow
や SkipNow
は、それらを呼び出したゴルーチンを runtime.Goexit()
を使って終了させます。しかし、これはそのゴルーチンのみに影響し、テスト関数が起動した他のゴルーチンには影響を与えません。もしテスト関数が複数のゴルーチンを起動し、そのうちの1つが FailNow
を呼び出した場合、そのゴルーチンは終了しますが、テスト関数自体(メインのテストゴルーチン)は引き続き実行され、他のゴルーチンも実行を続ける可能性があります。これにより、テストが期待通りに失敗またはスキップされなかったり、テストの終了が遅延したり、あるいはテストのロジックが複雑になったりする問題が発生する可能性がありました。
コミットメッセージにある "Fixes #3800" は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。Issue #3800は、FailNow
がテストのメインゴルーチンから呼び出されない場合に、テストがハングアップする可能性があるという問題について議論していました。このコミットは、ランタイムレベルでの厳密なチェックが困難であるため、少なくともドキュメントとしてこの重要な制約を明記することで、開発者が正しい使い方を理解し、潜在的な問題を回避できるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と testing
パッケージの基本的な知識が必要です。
-
ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。
go
キーワードを使って関数を呼び出すことで、新しいゴルーチンが起動されます。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数千、数万のゴルーチンを同時に実行することが可能です。Goの並行処理の根幹をなす要素です。 -
testing
パッケージ: Go言語に組み込まれているテストフレームワークです。go test
コマンドを通じて利用され、ユニットテスト、ベンチマークテスト、サンプルテストなどを記述するための機能を提供します。*testing.T
: テスト関数に渡される構造体で、テストのログ出力、エラー報告、テストのスキップなどの機能を提供します。Fail()
: テストを失敗としてマークしますが、テスト関数の実行は継続します。FailNow()
: テストを失敗としてマークし、現在のゴルーチンの実行を即座に停止します。これにより、テスト関数はそれ以降の処理を実行せず、次のテストまたはベンチマークに移行します。Skip()
: テストをスキップとしてマークしますが、テスト関数の実行は継続します。SkipNow()
: テストをスキップとしてマークし、現在のゴルーチンの実行を即座に停止します。これにより、テスト関数はそれ以降の処理を実行せず、次のテストまたはベンチマークに移行します。runtime.Goexit()
:FailNow
やSkipNow
の内部で使われる関数で、現在のゴルーチンを終了させます。これはpanic
とは異なり、スタックをアンワインドせず、defer
関数は実行されますが、呼び出し元の関数には戻りません。
-
並行処理におけるテストの課題: テスト対象のコードが並行処理(複数のゴルーチン)を使用している場合、テストコード自体も並行処理の特性を考慮する必要があります。特に、テストの失敗やスキップをトリガーする関数が、テストのメインゴルーチンとは異なるゴルーチンから呼び出された場合、テストフレームワークがその状態を正しく認識できない、あるいはテストの終了を適切に管理できないという問題が発生しがちです。
技術的詳細
このコミットの技術的な詳細は、testing
パッケージの FailNow
と SkipNow
の動作原理と、Goのゴルーチンモデルとの相互作用に焦点を当てています。
FailNow
と SkipNow
は、内部で runtime.Goexit()
を呼び出すことで、現在のゴルーチンの実行を停止します。runtime.Goexit()
は、現在のゴルーチンを終了させるためのGoランタイムのプリミティブです。これは panic
とは異なり、スタックをアンワインドしてパニックリカバリをトリガーするのではなく、単に現在のゴルーチンを終了させます。これにより、defer
ステートメントは実行されますが、呼び出し元の関数への通常の戻りはありません。
問題は、テスト関数が go
キーワードを使って新しいゴルーチンを起動し、その新しいゴルーチン内で FailNow
や SkipNow
を呼び出した場合です。この場合、FailNow
や SkipNow
はその新しいゴルーチンを終了させますが、テスト関数を実行しているメインのゴルーチンは影響を受けません。結果として、テスト関数は引き続き実行され、テストフレームワークはテストが失敗またはスキップされたことを即座に認識できません。これは、テストのロジックが複雑になったり、テストが期待通りに終了しなかったりする原因となります。
このコミットでは、この動作がGoランタイムの設計上の特性であり、testing
パッケージがこれをプログラム的に(例えば、FailNow
がメインゴルーチン以外から呼び出された場合にエラーを出すなど)チェックすることは「ランタイムにずる賢く踏み込まない限り」不可能であると述べています。これは、Goのランタイムがゴルーチンの親子関係や呼び出し元のコンテキストを直接的に公開するAPIを提供していないため、testing
パッケージが現在のゴルーチンがテストのメインゴルーチンであるかどうかを判断する標準的な方法がないことを意味します。
したがって、このコミットは、コードによる強制ではなく、ドキュメントによる明確なガイドラインを提供することで、この問題を解決しようとしています。開発者に対して、FailNow
と SkipNow
は必ずテストまたはベンチマーク関数を実行しているゴルーチン(通常は go test
が起動するメインのテストゴルーチン)から呼び出すべきであるというルールを明示的に伝えています。これにより、開発者はテストコードを記述する際にこの制約を意識し、テストの信頼性と予測可能性を向上させることができます。
また、コメントの修正(例: Println()
から Println
への変更)は、Goのドキュメンテーションスタイルガイドラインに沿ったもので、関数名を括弧なしで参照する慣習に合わせたものです。これは機能的な変更ではなく、コードの可読性と一貫性を向上させるためのものです。
コアとなるコードの変更箇所
変更は src/pkg/testing/testing.go
ファイルに集中しています。
-
FailNow()
関数のコメント追加:--- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -212,6 +212,10 @@ func (c *common) Failed() bool { // FailNow marks the function as having failed and stops its execution. // Execution will continue at the next test or benchmark. +// FailNow must be called from the goroutine running the +// test or benchmark function, not from other goroutines +// created during the test. Calling FailNow does not stop +// those other goroutines. func (c *common) FailNow() { c.Fail()
-
SkipNow()
関数のコメント追加:--- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -345,20 +349,23 @@ func (t *T) report() { } } -// Skip is equivalent to Log() followed by SkipNow(). +// Skip is equivalent to Log followed by SkipNow. func (t *T) Skip(args ...interface{}) { t.log(fmt.Sprintln(args...)) t.SkipNow() } -// Skipf is equivalent to Logf() followed by SkipNow(). +// Skipf is equivalent to Logf followed by SkipNow. func (t *T) Skipf(format string, args ...interface{}) { t.log(fmt.Sprintf(format, args...)) t.SkipNow() } -// SkipNow marks the function as having been skipped and stops its execution. -// Execution will continue at the next test or benchmark. See also, t.FailNow. +// SkipNow marks the test as having been skipped and stops its execution. +// Execution will continue at the next test or benchmark. See also FailNow. +// SkipNow must be called from the goroutine running the test, not from +// other goroutines created during the test. Calling SkipNow does not stop +// those other goroutines. func (t *T) SkipNow() { t.skip() runtime.Goexit() @@ -370,7 +377,7 @@ func (t *T) skip() { t.skipped = true } -// Skipped reports whether the function was skipped. +// Skipped reports whether the test was skipped. func (t *T) Skipped() bool { t.mu.RLock() defer t.mu.RUnlock()
-
コメント内の関数名表記の修正:
Log()
,Logf()
,Error()
,Errorf()
,Fatal()
,Fatalf()
などの参照が、括弧なしのLog
,Logf
などに変更されています。これはスタイルガイドラインに合わせたものです。
コアとなるコードの解説
このコミットの主要な変更は、FailNow
と SkipNow
関数のドキュメンテーションコメントに、これらの関数が「テストまたはベンチマーク関数を実行しているゴルーチンから呼び出されなければならない」という制約を追加した点です。
-
FailNow()
の変更: 既存のコメントに加えて、以下の4行が追加されました。// FailNow must be called from the goroutine running the // test or benchmark function, not from other goroutines // created during the test. Calling FailNow does not stop // those other goroutines.
これは、
FailNow
が呼び出されたゴルーチンのみを停止させ、テスト中にgo
キーワードで起動された他のゴルーチンには影響を与えないことを明確にしています。これにより、開発者はテストが期待通りに終了しない、あるいはテストの失敗が適切に報告されないといった問題を回避できます。 -
SkipNow()
の変更: 同様に、SkipNow
のコメントにも以下の3行が追加されました。// SkipNow must be called from the goroutine running the test, not from // other goroutines created during the test. Calling SkipNow does not stop // those other goroutines.
FailNow
と同様に、SkipNow
も呼び出されたゴルーチンのみを停止させ、他のゴルーチンには影響を与えないことを強調しています。これにより、テストのスキップが正しく機能し、テストスイートの実行がスムーズに行われるようになります。
これらのドキュメントの追加は、Goの testing
パッケージの設計上の制約と、並行処理環境でのテストの複雑さを開発者に伝える上で非常に重要です。Goのランタイムがこの制約を自動的にチェックできないため、ドキュメントによる明確な指示が、開発者が堅牢で予測可能なテストを書くための唯一の手段となります。
コメント内の関数名表記の修正は、Goのドキュメンテーションにおける慣習に合わせたもので、機能的な意味合いはありません。例えば、Log()
が Log
に変更されたのは、関数名をテキストで参照する際に括弧を省略するというスタイルに統一するためです。
関連リンク
- Go言語の
testing
パッケージのドキュメント: https://pkg.go.dev/testing - Go言語のゴルーチンに関するドキュメント: https://go.dev/doc/effective_go#concurrency
- Go言語の
runtime.Goexit()
のドキュメント: https://pkg.go.dev/runtime#Goexit
参考にした情報源リンク
- Go Issue #3800:
testing.T.FailNow
does not stop test if called from goroutine: https://github.com/golang/go/issues/3800 - Go Code Review 7268043:
testing: SkipNow, FailNow must be called from test goroutine
: https://golang.org/cl/7268043 - Goのドキュメンテーションスタイルガイドライン(一般的な慣習として)