[インデックス 16124] ファイルの概要
このコミットは、Go言語のランタイム、デバッグ、およびデータ競合検出に関連するテストの不安定性(flakiness)を解消することを目的としています。具体的には、テスト実行時の環境変数の影響を排除し、ガベージコレクション(GC)の設定がテスト結果に与える副作用を抑制することで、テストの信頼性を向上させています。
コミット
commit 4235fa8f2a1e3b9f162a477b7ce210b98e84eb65
Author: Albert Strasheim <fullung@gmail.com>
Date: Sun Apr 7 11:37:37 2013 -0700
runtime, runtime/debug, runtime/race: deflake tests
R=golang-dev, dvyukov, bradfitz
CC=golang-dev
https://golang.org/cl/8366044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4235fa8f2a1e3b9f162a477b7ce210b98e84eb65
元コミット内容
runtime, runtime/debug, runtime/race: deflake tests
変更の背景
ソフトウェア開発において、テストはコードの品質と信頼性を保証するために不可欠です。しかし、「flaky test(不安定なテスト)」と呼ばれる現象が発生することがあります。これは、コードの変更がないにもかかわらず、テストが成功したり失敗したりする予測不可能な挙動を示すテストを指します。不安定なテストは、開発者の生産性を著しく低下させ、CI/CDパイプラインの信頼性を損ない、誤ったアラートによって本質的な問題を見逃す原因となります。
このコミットの背景には、Go言語のランタイム、デバッグ、およびデータ競合検出に関連するテストスイートが、特定の環境要因や実行順序によって不安定になっていたという問題があります。特に、GOGCTRACE
環境変数の出力がテストの標準出力と混ざり合い、テストが期待する出力を正しくパースできないことが一因となっていました。また、ガベージコレクションの挙動がテストの実行に影響を与え、結果を不安定にすることも考えられます。
このコミットは、これらの不安定性の根本原因に対処し、テストスイート全体の信頼性と予測可能性を高めることを目的としています。これにより、開発者はテスト結果をより信頼し、迅速かつ自信を持ってコード変更を進めることができるようになります。
前提知識の解説
不安定なテスト (Flaky Tests)
不安定なテストとは、同じコードベースに対して複数回実行されたときに、成功と失敗を交互に繰り返すテストのことです。その原因は多岐にわたりますが、主なものとしては以下が挙げられます。
- 環境依存性: テストが実行される環境(OS、環境変数、ファイルシステムの状態など)に依存している場合。
- 並行処理の問題: 複数のゴルーチンやスレッドが共有リソースにアクセスする際の競合状態(race condition)が、テストの実行タイミングによって異なる結果を生む場合。
- 時間依存性: テストが特定の時間的制約(タイムアウト、非同期処理の完了待ちなど)に依存しており、実行時のシステム負荷やスケジューリングによって結果が変わる場合。
- 外部サービス依存性: データベース、ネットワークサービス、APIなどの外部依存が不安定である場合。
- リソースリーク: テストがリソース(メモリ、ファイルハンドルなど)を適切に解放せず、後続のテストに影響を与える場合。
不安定なテストは、開発者がテスト結果を信用できなくなり、CI/CDパイプラインが頻繁に失敗するため、開発プロセスを遅延させる深刻な問題です。
GOGCTRACE
環境変数
GOGCTRACE
はGo言語のランタイムが提供する環境変数の一つで、ガベージコレクション(GC)の動作に関する詳細なトレース情報を標準エラー出力(stderr)に出力するために使用されます。この変数を設定すると、GCの実行タイミング、所要時間、ヒープサイズの変化、オブジェクトの解放状況など、GCの内部挙動に関する情報がリアルタイムで出力されます。
開発者やデバッグ担当者は、この情報を用いてGCのパフォーマンスを分析したり、メモリリークやGCの頻発といった問題を特定したりします。しかし、テストが標準出力や標準エラー出力をパースして結果を検証する場合、GOGCTRACE
による余分な出力がテストの期待する出力と混ざり合い、パースエラーや誤ったテスト結果を引き起こす可能性があります。これが、テストが不安定になる一因となることがあります。
GCPercent
GCPercent
はGo言語のガベージコレクタの動作を制御する重要なパラメータの一つです。これは、前回のGCが完了した時点のライブヒープサイズに対して、どれだけの新しいメモリが割り当てられたら次のGCをトリガーするかをパーセンテージで指定します。デフォルト値は100で、これは前回のGC後にヒープサイズが2倍になったら次のGCを実行するという意味です。
GCPercent
の値を調整することで、GCの頻度とメモリ使用量のバランスを制御できます。値を小さくするとGCが頻繁に実行され、メモリ使用量は抑えられますが、GCによるアプリケーションの一時停止(stop-the-world)が増える可能性があります。逆に値を大きくするとGCの頻度は減り、アプリケーションの実行はスムーズになりますが、メモリ使用量が増加する可能性があります。
テストコード内でGCPercent
が変更されると、その変更が他のテストやシステム全体のGC挙動に影響を与え、テストの再現性を損なう可能性があります。そのため、テスト内でGCPercent
を変更した場合は、テスト終了後に元の値に戻すなどのクリーンアップ処理が重要になります。
技術的詳細
このコミットは、Go言語のテストフレームワークにおける不安定なテストの問題に対処するために、以下の技術的アプローチを採用しています。
-
環境変数のフィルタリング:
src/pkg/runtime/crash_test.go
とsrc/pkg/runtime/race/race_test.go
において、exec.Command
で外部プロセスを実行する際に、GOGCTRACE
環境変数を意図的に除外するtestEnv
ヘルパー関数が導入されました。これにより、GCトレース情報がテストの標準出力に混入し、テストが期待する出力を正しくパースできなくなる問題を回避します。これは、テストが外部コマンドの出力を厳密に検証する場合に特に重要です。GOMAXPROCS
も同様に除外されることで、CPUコア数の設定がテストの並行処理に与える影響を標準化し、テストの再現性を高めています。 -
GC設定のリセット:
src/pkg/runtime/debug/garbage_test.go
のTestReadGCStats
関数において、defer SetGCPercent(SetGCPercent(-1))
という行が追加されました。SetGCPercent
関数は、GoランタイムのGCPercent
設定を変更し、その変更前の値を返します。このdefer
ステートメントは、TestReadGCStats
関数が終了する際に、SetGCPercent(-1)
を呼び出すことで、GCPercent
をデフォルト値(またはテスト開始前の値)にリセットします。SetGCPercent(-1)
は、GCPercent
を無効にするのではなく、Go 1.5以降ではGCを無効にする特別な値として扱われますが、このコミットの時点(Go 1.0.3リリース後)では、おそらくGCをデフォルトの挙動に戻すか、テストに影響を与えない値に設定する意図があったと考えられます。これにより、このテストがGCPercent
を変更しても、その変更が他のテストに影響を与え、テストスイート全体が不安定になることを防ぎます。
これらの変更は、テストが外部環境やランタイムの内部状態に過度に依存しないようにすることで、テストの独立性と再現性を高め、不安定なテストの問題を根本的に解決しようとしています。
コアとなるコードの変更箇所
src/pkg/runtime/crash_test.go
--- a/src/pkg/runtime/crash_test.go
+++ b/src/pkg/runtime/crash_test.go
@@ -14,6 +14,22 @@ import (
"text/template"
)
+// testEnv excludes GOGCTRACE from the environment
+// to prevent its output from breaking tests that
+// are trying to parse other command output.
+func testEnv(cmd *exec.Cmd) *exec.Cmd {
+ if cmd.Env != nil {
+ panic("environment already set")
+ }
+ for _, env := range os.Environ() {
+ if strings.HasPrefix(env, "GOGCTRACE=") {
+ continue
+ }
+ cmd.Env = append(cmd.Env, env)
+ }
+ return cmd
+}
+
func executeTest(t *testing.T, templ string, data interface{}) string {
checkStaleRuntime(t)
@@ -37,13 +53,13 @@ func executeTest(t *testing.T, templ string, data interface{}) string {
}\n\tf.Close()\n\n-\tgot, _ := exec.Command("go", "run", src).CombinedOutput()\n+\tgot, _ := testEnv(exec.Command("go", "run", src)).CombinedOutput()\n \treturn string(got)\n }\n \n func checkStaleRuntime(t *testing.T) {\n \t// 'go run' uses the installed copy of runtime.a, which may be out of date.\n-\tout, err := exec.Command("go", "list", "-f", "{{.Stale}}", "runtime").CombinedOutput()\n+\tout, err := testEnv(exec.Command("go", "list", "-f", "{{.Stale}}", "runtime")).CombinedOutput()\n \tif err != nil {\n \t\tt.Fatalf("failed to execute 'go list': %v\\n%v", err, string(out))\n \t}\n```
### `src/pkg/runtime/debug/garbage_test.go`
```diff
--- a/src/pkg/runtime/debug/garbage_test.go
+++ b/src/pkg/runtime/debug/garbage_test.go
@@ -11,6 +11,8 @@ import (
)
func TestReadGCStats(t *testing.T) {
+ defer SetGCPercent(SetGCPercent(-1))
+
var stats GCStats
var mstats runtime.MemStats
var min, max time.Duration
src/pkg/runtime/race/race_test.go
--- a/src/pkg/runtime/race/race_test.go
+++ b/src/pkg/runtime/race/race_test.go
@@ -147,7 +147,7 @@ func runTests() ([]byte, error) {
// It is required because the tests contain a lot of data races on the same addresses
// (the tests are simple and the memory is constantly reused).
for _, env := range os.Environ() {
-\t\tif strings.HasPrefix(env, "GOMAXPROCS=") {\n+\t\tif strings.HasPrefix(env, "GOMAXPROCS=") || strings.HasPrefix(env, "GOGCTRACE=") {\n \t\t\tcontinue
\t\t}
\t\tcmd.Env = append(cmd.Env, env)
コアとなるコードの解説
src/pkg/runtime/crash_test.go
の変更
-
testEnv
関数の追加: この新しいヘルパー関数は、*exec.Cmd
を受け取り、そのEnv
フィールドを構築します。os.Environ()
から現在の環境変数を取得し、GOGCTRACE=
で始まる環境変数をスキップして、新しい環境変数リストを作成します。これにより、go run
やgo list
などのコマンドが実行される際に、GOGCTRACE
によるGCトレース出力が抑制され、テストの標準出力がクリーンに保たれます。これは、テストがコマンドの出力をパースして特定の文字列を期待する場合に、余分な出力がテストを不安定にするのを防ぐための重要な変更です。 -
exec.Command
の呼び出しの変更:executeTest
関数とcheckStaleRuntime
関数内のexec.Command
の呼び出しが、新しく追加されたtestEnv
関数でラップされるようになりました。got, _ := testEnv(exec.Command("go", "run", src)).CombinedOutput()
out, err := testEnv(exec.Command("go", "list", "-f", "{{.Stale}}", "runtime")).CombinedOutput()
これにより、これらのテストが実行する外部Goコマンドは、GOGCTRACE
の影響を受けずに実行され、テストの出力が予測可能になります。
src/pkg/runtime/debug/garbage_test.go
の変更
TestReadGCStats
関数へのdefer SetGCPercent(SetGCPercent(-1))
の追加:TestReadGCStats
関数の冒頭にdefer SetGCPercent(SetGCPercent(-1))
という行が追加されました。SetGCPercent(-1)
は、GoのガベージコレクタのGCPercent
設定を-1
に設定します。この値は、GCを無効にするか、特別な挙動をトリガーするために使用されることがあります(Goのバージョンによって挙動が異なる可能性がありますが、このコミットの時点では、テストに影響を与えないようにGCの挙動をリセットする意図があったと考えられます)。SetGCPercent(...)
は、変更前のGCPercent
の値を返します。- 外側の
SetGCPercent
は、この返された値を引数として受け取り、再度GCPercent
を設定します。 defer
キーワードにより、この行はTestReadGCStats
関数が終了する直前に実行されます。 この構造により、TestReadGCStats
関数内でGCPercent
が変更されたとしても、関数終了時には元のGCPercent
の値に確実にリセットされます。これにより、このテストがGCの挙動を変更したことによる副作用が、後続のテストに影響を与え、テストスイート全体が不安定になることを防ぎます。
src/pkg/runtime/race/race_test.go
の変更
runTests
関数内の環境変数フィルタリングの拡張:runTests
関数内で、外部コマンドの環境変数を設定するループにおいて、GOMAXPROCS=
に加えてGOGCTRACE=
で始まる環境変数もスキップされるようになりました。if strings.HasPrefix(env, "GOMAXPROCS=") || strings.HasPrefix(env, "GOGCTRACE=") { continue }
これは、runtime/crash_test.go
と同様に、データ競合検出テストの実行中にGOGCTRACE
の出力が混入し、テストのパースを妨げるのを防ぐための変更です。GOMAXPROCS
も除外することで、テストの並行処理の挙動を標準化し、テストの再現性を高めています。
これらの変更は、テストが実行される環境やランタイムの内部状態からの干渉を最小限に抑えることで、テストの独立性と信頼性を大幅に向上させることを目的としています。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Go言語のガベージコレクションに関するドキュメント(当時の情報に基づく)
- Go言語のテストに関するドキュメント
参考にした情報源リンク
- Go言語のソースコード(コミット履歴と関連ファイル)
- Go言語のIssueトラッカーやメーリングリスト(当時の議論や関連するバグ報告)
- 一般的なソフトウェアテストのプラクティスと不安定なテストに関する情報