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

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

このコミットは、Go言語のテストスイートにおけるtest/fixedbugs/issue4388.goという特定のテストケースが、Plan 9環境で不安定(flakey)になる問題を修正するものです。具体的には、runtime.Caller関数が返すスタックフレーム情報、特に<autogenerated>:1という特殊なファイル名と行番号の組み合わせに関する挙動が、Plan 9上で期待通りにならない場合があるため、テストのロジックをより堅牢にする変更が加えられています。

コミット

  • コミットハッシュ: 147a21456e2b2255d1c7e487cb9f53386791c357
  • Author: Mikio Hara mikioh.mikioh@gmail.com
  • Date: Thu May 15 06:39:15 2014 +0900

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

https://github.com/golang/go/commit/147a21456e2b2255d1c7e487cb9f53386791c357

元コミット内容

    test: fix flakey test case for issue 4388
    
    Seems like we need to drag the stack for <autogenerated>:1 on Plan 9.
    
    See http://build.golang.org/log/283b996102b833dd81c58301d78aceaa4fe9838b.
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/95390043

変更の背景

この変更の背景には、Go言語のテスト環境における特定の不安定性があります。コミットメッセージにある「flakey test case for issue 4388」は、テストが時々失敗し、時々成功するという非決定的な挙動を示していたことを意味します。これは、テストが特定の環境やタイミングに依存して結果が変わる場合に発生します。

具体的には、runtime.Caller関数がスタックフレーム情報を取得する際に、<autogenerated>:1という特殊なソース位置を期待するテストが、Plan 9という特定のオペレーティングシステム環境で不安定になっていました。コミットメッセージの「Seems like we need to drag the stack for :1 on Plan 9.」という記述は、Plan 9上でのスタックの挙動が他のOSと異なり、runtime.Callerが期待するスタックフレームをすぐに見つけられない場合があることを示唆しています。

「issue 4388」の具体的な内容は公開されているGoのIssueトラッカーからは直接確認できませんでしたが、これは内部的なバグトラッキングシステムでの識別子であるか、あるいは非常に古い、現在はアーカイブされている問題である可能性があります。しかし、コミットメッセージとコードの変更内容から、runtime.Callerの挙動とスタックトレースの解析に関する問題であったことは明らかです。

前提知識の解説

1. runtime.Caller関数

runtime.Caller(skip int)は、Go言語のruntimeパッケージが提供する関数で、呼び出し元の関数のファイル名、行番号、および関数名を取得するために使用されます。skip引数は、スタックフレームをどれだけスキップするかを指定します。skip=0Caller関数自身の呼び出し元、skip=1Callerを呼び出した関数の呼び出し元、といった具合です。

例えば、以下のようなコードがあったとします。

package main

import (
	"fmt"
	"runtime"
)

func foo() {
	bar()
}

func bar() {
	_, file, line, _ := runtime.Caller(1) // bar()の呼び出し元 (foo) の情報を取得
	fmt.Printf("Called from: %s:%d\n", file, line)
}

func main() {
	foo()
}

この場合、bar()内でruntime.Caller(1)を呼び出すと、foo()の呼び出し元の情報(main.gofoo()が呼び出された行)が取得されます。

2. スタックトレースとスタックフレーム

プログラムが実行される際、関数呼び出しの履歴は「コールスタック(Call Stack)」に積まれていきます。各関数呼び出しは「スタックフレーム(Stack Frame)」としてスタックに記録され、そのフレームには関数の引数、ローカル変数、戻りアドレスなどの情報が含まれます。runtime.Callerは、このコールスタックを遡って特定のスタックフレームの情報を取得するメカニズムを提供します。

3. <autogenerated>:1

Go言語のコンパイラやランタイムは、デバッグ情報やプロファイリングのために、ソースコードには直接存在しない「自動生成された」コードのスタックフレームを生成することがあります。これらのフレームは、実際のファイルパスを持たないため、<autogenerated>という特別なファイル名と、通常は1という行番号で識別されます。これは、Goの内部的な処理や、特定の最適化、あるいはテストハーネスのようなメタプログラミング的な状況で現れることがあります。

4. Plan 9

Plan 9 from Bell Labsは、ベル研究所で開発された分散オペレーティングシステムです。Go言語の設計者の一部(Ken ThompsonやRob Pikeなど)はPlan 9の開発にも深く関わっており、Go言語のツールチェイン(アセンブラ、コンパイラ、リンカ)やアセンブラの構文は、Plan 9の対応するものから直接派生しています。

Goのランタイム、特にスタック管理にはPlan 9の影響が色濃く残っています。Goのゴルーチンは動的にリサイズ可能なスタックを使用しますが、Plan 9のような特定のOSでは、OS固有の関数(シグナルハンドリングなど)のためにゴルーチンのスタック上に固定サイズの領域を予約するといった、OS固有のスタック割り当ての考慮事項があります。このため、runtime.Callerのようなスタックを操作する関数が、Plan 9環境で他のOSとは異なる挙動を示す可能性がありました。

技術的詳細

このコミットが修正しようとしている問題は、runtime.Caller<autogenerated>:1という特定のスタックフレームを期待する際に、Plan 9環境ではそのフレームが期待されるskipレベル(n)に存在しない場合があるというものです。

元のコードでは、runtime.Caller(n)を一度だけ呼び出し、その結果が<autogenerated>:1であるかをチェックしていました。しかし、Plan 9のスタックの特性(「drag the stack」という表現が示唆するように、スタックフレームの配置や深さが他のOSと異なる、あるいは動的に変化する可能性がある)により、期待する<autogenerated>:1のフレームが、nで指定された正確な位置に常に存在するとは限りませんでした。場合によっては、より深いスタックレベルに隠れている可能性があったのです。

この問題を解決するため、変更後のコードではruntime.Callerをループ内で複数回呼び出すように修正されています。具体的には、i1からnまでインクリメントしながらruntime.Caller(i)を呼び出し、それぞれの呼び出しで返されるファイル名と行番号が<autogenerated>:1と一致するかをチェックします。これにより、期待する<autogenerated>:1のフレームがスタックのどこか(1からnの範囲内)に存在すれば、テストは成功するようになります。

これは、特定のスタックフレームが常に固定のskipレベルにあるとは限らないという、OSやコンパイラの挙動に起因する非決定性を吸収するための堅牢なアプローチです。特に、テストコードではこのような非決定的な挙動を排除し、安定した結果を得ることが非常に重要です。

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

変更はtest/fixedbugs/issue4388.goファイル内のcheckLine関数に集中しています。

--- a/test/fixedbugs/issue4388.go
+++ b/test/fixedbugs/issue4388.go
@@ -43,8 +43,14 @@ func checkLine(n int) {
 	if err := recover(); err == nil {
 		panic("did not panic")
 	}
-	_, file, line, _ := runtime.Caller(n)
-	if file != "<autogenerated>" || line != 1 {
-		panic(fmt.Sprintf("expected <autogenerated>:1 have %s:%d", file, line))
+	var file string
+	var line int
+	for i := 1; i <= n; i++ {
+		_, file, line, _ = runtime.Caller(i)
+		if file != "<autogenerated>" || line != 1 {
+			continue
+		}
+		return
 	}
+	panic(fmt.Sprintf("expected <autogenerated>:1 have %s:%d", file, line))
 }

コアとなるコードの解説

変更されたcheckLine関数は、以下のロジックで動作します。

  1. パニックの確認: まず、recover()を使ってパニックが発生したことを確認します。パニックが発生していない場合はテスト失敗とします。
  2. スタックフレームの探索:
    • fileline変数を宣言します。
    • for i := 1; i <= n; i++というループを開始します。このループは、runtime.Callerskip引数を1からnまで順に試行します。
    • ループ内で_, file, line, _ = runtime.Caller(i)を呼び出し、現在のiレベルでのスタックフレーム情報を取得します。
    • 取得したfile"<autogenerated>"であり、かつline1であるかをチェックします。
    • もし条件が一致すれば、期待するスタックフレームが見つかったことになるため、関数をreturnして正常終了します。
    • 条件が一致しない場合はcontinueし、次のiの値でスタックをさらに深く探索します。
  3. エラーハンドリング: ループがnまで到達しても<autogenerated>:1のスタックフレームが見つからなかった場合、panicを発生させ、期待するフレームが見つからなかったことを報告します。これにより、テストは失敗します。

この変更により、runtime.Callerが返すスタックフレームの正確なskipレベルがPlan 9環境で変動しても、テストが堅牢に動作するようになりました。テストは、1からnまでの任意のskipレベルで<autogenerated>:1が見つかれば成功と判断するため、より柔軟に対応できます。

関連リンク

  • Go Change-ID: https://golang.org/cl/95390043
  • ビルドログ: http://build.golang.org/log/283b996102b833dd81c58301d78aceaa4fe9838b

参考にした情報源リンク