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

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

このコミットは、Go言語の標準ライブラリ time パッケージ内の sleep_test.go ファイルに対する変更です。具体的には、TestReset というテスト関数の信頼性を向上させることを目的としています。

コミット

  • コミットハッシュ: 86a8d59a014287c899a14ef9ed6fdfb7d1b8d586
  • 作者: Brad Fitzpatrick bradfitz@golang.org
  • コミット日時: 2013年1月22日 火曜日 17:25:58 -0800

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

https://github.com/golang/go/commit/86a8d59a014287c899a14ef9ed6fdfb7d1b8d586

元コミット内容

time: make TestReset more reliable

Fixes #4690

R=golang-dev, alex.brainman, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/7181052

変更の背景

このコミットの背景には、Go言語の time パッケージにおける Timer.Reset メソッドのテスト TestReset が、特定の環境下で不安定(flaky)になるという問題がありました。コミットメッセージにある Fixes #4690 は、この問題がGoのIssueトラッカーで報告されていたことを示しています。

不安定なテスト(flaky test)とは、コードの変更がないにもかかわらず、実行するたびに成功したり失敗したりするテストのことです。このようなテストは、CI/CDパイプラインの信頼性を損ない、開発者が実際のバグとテストの不安定さを区別するのを困難にします。time パッケージのテストは、時間的な要素に依存するため、システム負荷、CPUスケジューリング、タイマーの精度など、環境要因によって結果が変動しやすい傾向があります。

元の TestReset は、固定されたミリ秒単位のDuration(例: 100 * Millisecond)を使用してタイマーをリセットし、その動作を検証していました。しかし、これは実行環境のパフォーマンスやタイマーの粒度によっては、期待通りのタイミングでタイマーが発火しない、あるいは発火しすぎるなどの問題を引き起こす可能性がありました。特に、低速なハードウェアや高負荷なシステムでは、テストが意図した時間間隔で実行されず、誤って失敗するケースが考えられます。

このコミットは、このような環境依存の不安定さを解消し、TestReset がより堅牢に、かつ様々な実行環境で安定して動作するように改善することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とテストに関する知識が必要です。

  1. time パッケージ: Go言語の標準ライブラリで、時間に関する機能(時刻、期間、タイマー、Tickerなど)を提供します。

    • time.Duration: 時間の長さを表す型です。ミリ秒、秒、分などの単位で期間を指定できます。
    • time.NewTimer(d Duration): 指定された期間 d が経過した後に、現在の時刻をチャネル C に送信する新しい Timer を作成します。タイマーは一度だけ発火します。
    • Timer.C: Timer が発火したときに時刻が送信される読み取り専用チャネルです。
    • Timer.Reset(d Duration): タイマーをリセットし、新しい期間 d が経過した後に発火するように設定します。
      • Reset メソッドは bool 値を返します。
        • true を返す場合: タイマーがまだ発火しておらず、チャネル C に値が送信されていない状態でリセットに成功したことを意味します。
        • false を返す場合: タイマーが既に発火しているか、チャネル C から値が読み取られた後にリセットされたことを意味します。この場合、Reset は新しいタイマーを作成するのと同等に動作します。
    • time.Sleep(d Duration): 指定された期間 d だけ現在のゴルーチンをスリープさせます。
  2. select ステートメント: 複数のチャネル操作を待機するために使用されます。

    • case <-t0.C:: t0.C チャネルから値が受信できるまで待機します。
    • default:: select ブロック内のどのチャネル操作もすぐに実行できない場合に実行されます。これにより、チャネル操作がブロックされるのを防ぎ、非ブロック的なチャネルの読み取りや書き込みが可能になります。
  3. Go言語のテスト: testing パッケージを使用してテストを記述します。

    • func TestXxx(t *testing.T): テスト関数は Test で始まり、*testing.T 型の引数を取ります。
    • t.Fatalf(...): テストを失敗させ、メッセージを出力してテストの実行を停止します。
    • t.Error(...): テストを失敗させ、メッセージを出力しますが、テストの実行は継続します。
    • t.Logf(...): テストのログにメッセージを出力します。テストが成功した場合でも出力されます。
  4. Flaky Test (不安定なテスト): 前述の通り、実行するたびに結果が変わるテストのことです。時間依存のテスト、並行処理のテスト、外部サービスに依存するテストなどで発生しやすいです。これを解決するためには、テストのロジックをより堅牢にするか、テスト環境の変動を吸収するメカニズムを導入する必要があります。

技術的詳細

元の TestReset 関数は、固定された Duration 値(例: 100 * Millisecond)を使用してタイマーの動作を検証していました。このアプローチは、システムクロックの粒度やスケジューリングの変動に対して脆弱でした。例えば、Sleep(50 * Millisecond) の後に Reset(150 * Millisecond) を呼び出し、その後 Sleep(100 * Millisecond) を行うというシーケンスでは、厳密なタイミングが要求されます。もし Sleep がわずかに長くかかったり、タイマーの発火が遅れたりすると、テストが意図せず失敗する可能性がありました。

新しいアプローチでは、この不安定さを解消するために以下の変更が導入されました。

  1. testReset ヘルパー関数の導入:

    • 元の TestReset のロジックを testReset(d Duration) error というヘルパー関数に切り出しました。
    • このヘルパー関数は、テストで使用する基本となる時間単位 d を引数として受け取ります。
    • タイマーの期間やスリープ時間は、この d の倍数として定義されます(例: 2 * d, 3 * d)。これにより、テストの実行時間を柔軟に調整できるようになります。
    • テストが失敗した場合、t.Fatalf の代わりに errors.New を使用してエラーを返します。これにより、呼び出し元の TestReset 関数がエラーを捕捉し、再試行のロジックを実装できるようになります。
  2. 複数回の試行による堅牢化:

    • 新しい TestReset 関数は、testReset ヘルパー関数を異なる Duration 値で複数回試行するロジックを導入しました。
    • tries := []Duration{1 * unit, 3 * unit, 7 * unit, 15 * unit} という配列が定義されています。unit25 * Millisecond です。
    • テストは、最も短い Duration (25ms) から順に testReset を実行します。
    • もし testReset がエラーなく成功した場合、その Duration でテストがパスしたと判断し、それ以上の試行は行いません。
    • これにより、高速なマシンでは短い Duration でテストがすぐに完了し、低速なマシンや高負荷な環境ではより長い Duration を試すことで、テストが安定してパスする可能性が高まります。これは、テストが「遅い、負荷の高いハードウェアでも不安定にならないように、しかし高速なマシンでは不必要に遅くならないように」という設計思想に基づいています。
    • すべての Duration で試行してもテストが成功しなかった場合のみ、最終的に t.Error(err) を呼び出してテストを失敗させます。

この変更により、TestReset は環境の変動に対してより耐性を持つようになり、Goのテストスイート全体の信頼性向上に貢献しています。

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

--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -247,26 +247,49 @@ func TestSleepZeroDeadlock(t *testing.T) {
 	<-c
 }
 
-func TestReset(t *testing.T) {
-	t0 := NewTimer(100 * Millisecond)
-	Sleep(50 * Millisecond)
-	if t0.Reset(150*Millisecond) != true {
-		t.Fatalf("resetting unfired timer returned false")
+func testReset(d Duration) error {
+	t0 := NewTimer(2 * d)
+	Sleep(d)
+	if t0.Reset(3*d) != true {
+		return errors.New("resetting unfired timer returned false")
 	}
-	Sleep(100 * Millisecond)
+	Sleep(2 * d)
 	select {
 	case <-t0.C:
-		t.Fatalf("timer fired early")
+		return errors.New("timer fired early")
 	default:
 	}
-	Sleep(100 * Millisecond)
+	Sleep(2 * d)
 	select {
 	case <-t0.C:
 	default:
-		t.Fatalf("reset timer did not fire")
+		return errors.New("reset timer did not fire")
 	}
 
 	if t0.Reset(50*Millisecond) != false {
-		t.Fatalf("resetting expired timer returned true")
+		return errors.New("resetting expired timer returned true")
+	}
+	return nil
+}
+
+func TestReset(t *testing.T) {
+	// We try to run this test with increasingly larger multiples
+	// until one works so slow, loaded hardware isn't as flaky,
+	// but without slowing down fast machines unnecessarily.
+	const unit = 25 * Millisecond
+	tries := []Duration{
+		1 * unit,
+		3 * unit,
+		7 * unit,
+		15 * unit,
+	}
+	var err error
+	for _, d := range tries {
+		err = testReset(d)
+		if err == nil {
+			t.Logf("passed using duration %v", d)
+			return
+		}
 	}
+	t.Error(err)
 }

コアとなるコードの解説

testReset(d Duration) error 関数

この関数は、TestReset の主要なテストロジックをカプセル化しています。引数 d は、テストで使用される時間単位の基準となります。

  1. t0 := NewTimer(2 * d): 2 * d の期間で新しいタイマー t0 を作成します。
  2. Sleep(d): d の期間だけスリープします。この時点でタイマーはまだ発火していないはずです。
  3. if t0.Reset(3*d) != true: タイマーを 3 * d の期間でリセットします。タイマーがまだ発火していないため、Resettrue を返すはずです。もし false を返した場合、エラーを返します。
  4. Sleep(2 * d): さらに 2 * d だけスリープします。この時点で、リセットされたタイマーはまだ発火していないはずです(合計 d + 2*d = 3*d のスリープで、リセット後の期間 3*d に達する)。
  5. select { case <-t0.C: ... default: }:
    • case <-t0.C:: もしタイマーがこの時点で発火していたら(つまり、3*d よりも早く発火したら)、"timer fired early" というエラーを返します。
    • default:: タイマーがまだ発火していないことを確認します。
  6. Sleep(2 * d): さらに 2 * d だけスリープします。この時点で、リセットされたタイマーは発火しているはずです(合計 d + 2*d + 2*d = 5*d のスリープで、リセット後の期間 3*d を超えている)。
  7. select { case <-t0.C: ... default: }:
    • case <-t0.C:: タイマーが発火したことを確認します。
    • default:: もしタイマーが発火していなかったら、"reset timer did not fire" というエラーを返します。
  8. if t0.Reset(50*Millisecond) != false: 既に発火したタイマーをリセットしようとします。この場合、Resetfalse を返すはずです。もし true を返した場合、"resetting expired timer returned true" というエラーを返します。
  9. return nil: すべてのチェックが成功した場合、nil を返して成功を示します。

TestReset(t *testing.T) 関数

この関数は、testReset ヘルパー関数を呼び出し、テストの堅牢性を高めるためのロジックを含んでいます。

  1. const unit = 25 * Millisecond: 基本となる時間単位を 25ミリ秒 と定義します。
  2. tries := []Duration{1 * unit, 3 * unit, 7 * unit, 15 * unit}: testReset を試行する異なる時間単位の倍数を定義します。これにより、25ms, 75ms, 175ms, 375ms の順でテストが試行されます。
  3. var err error: testReset から返されるエラーを保持するための変数です。
  4. for _, d := range tries: tries 配列の各 Duration d に対してループを実行します。
  5. err = testReset(d): testReset ヘルパー関数を現在の d で呼び出します。
  6. if err == nil: もし testReset がエラーなく成功した場合(nil を返した場合)、
    • t.Logf("passed using duration %v", d): どの Duration でテストがパスしたかをログに出力します。
    • return: テストを終了し、成功とします。
  7. t.Error(err): ループが終了しても testReset が一度も成功しなかった場合(つまり、すべての Duration でエラーが返された場合)、最後に発生したエラーを t.Error で出力し、テストを失敗とします。

この構造により、テストはまず短い時間間隔で迅速に実行を試み、もしそれが不安定な環境で失敗した場合には、より長い時間間隔で再試行することで、テストの信頼性を大幅に向上させています。

関連リンク

  • Go Issue #4690: https://code.google.com/p/go/issues/detail?id=4690 (古いGoogle Codeのリンクですが、コミットメッセージに記載されています)
  • Gerrit Change-Id: https://golang.org/cl/7181052 (GoのコードレビューシステムGerritのリンク)

参考にした情報源リンク

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

このコミットは、Go言語の標準ライブラリ time パッケージ内の sleep_test.go ファイルに対する変更です。具体的には、TestReset というテスト関数の信頼性を向上させることを目的としています。

コミット

  • コミットハッシュ: 86a8d59a014287c899a14ef9ed6fdfb7d1b8d586
  • 作者: Brad Fitzpatrick bradfitz@golang.org
  • コミット日時: 2013年1月22日 火曜日 17:25:58 -0800

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

https://github.com/golang/go/commit/86a8d59a014287c899a14ef9ed6fdfb7d1b8d586

元コミット内容

time: make TestReset more reliable

Fixes #4690

R=golang-dev, alex.brainman, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/7181052

変更の背景

このコミットの背景には、Go言語の time パッケージにおける Timer.Reset メソッドのテスト TestReset が、特定の環境下で不安定(flaky)になるという問題がありました。コミットメッセージにある Fixes #4690 は、この問題がGoのIssueトラッカーで報告されていたことを示しています。

不安定なテスト(flaky test)とは、コードの変更がないにもかかわらず、実行するたびに成功したり失敗したりするテストのことです。このようなテストは、CI/CDパイプラインの信頼性を損ない、開発者が実際のバグとテストの不安定さを区別するのを困難にします。time パッケージのテストは、時間的な要素に依存するため、システム負荷、CPUスケジューリング、タイマーの精度など、環境要因によって結果が変動しやすい傾向があります。

元の TestReset は、固定されたミリ秒単位のDuration(例: 100 * Millisecond)を使用してタイマーをリセットし、その動作を検証していました。しかし、これは実行環境のパフォーマンスやタイマーの粒度によっては、期待通りのタイミングでタイマーが発火しない、あるいは発火しすぎるなどの問題を引き起こす可能性がありました。特に、低速なハードウェアや高負荷なシステムでは、テストが意図した時間間隔で実行されず、誤って失敗するケースが考えられます。

このコミットは、このような環境依存の不安定さを解消し、TestReset がより堅牢に、かつ様々な実行環境で安定して動作するように改善することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とテストに関する知識が必要です。

  1. time パッケージ: Go言語の標準ライブラリで、時間に関する機能(時刻、期間、タイマー、Tickerなど)を提供します。

    • time.Duration: 時間の長さを表す型です。ミリ秒、秒、分などの単位で期間を指定できます。
    • time.NewTimer(d Duration): 指定された期間 d が経過した後に、現在の時刻をチャネル C に送信する新しい Timer を作成します。タイマーは一度だけ発火します。
    • Timer.C: Timer が発火したときに時刻が送信される読み取り専用チャネルです。
    • Timer.Reset(d Duration): タイマーをリセットし、新しい期間 d が経過した後に発火するように設定します。
      • Reset メソッドは bool 値を返します。
        • true を返す場合: タイマーがまだ発火しておらず、チャネル C に値が送信されていない状態でリセットに成功したことを意味します。
        • false を返す場合: タイマーが既に発火しているか、チャネル C から値が読み取られた後にリセットされたことを意味します。この場合、Reset は新しいタイマーを作成するのと同等に動作します。
    • time.Sleep(d Duration): 指定された期間 d だけ現在のゴルーチンをスリープさせます。
  2. select ステートメント: 複数のチャネル操作を待機するために使用されます。

    • case <-t0.C:: t0.C チャネルから値が受信できるまで待機します。
    • default:: select ブロック内のどのチャネル操作もすぐに実行できない場合に実行されます。これにより、チャネル操作がブロックされるのを防ぎ、非ブロック的なチャネルの読み取りや書き込みが可能になります。
  3. Go言語のテスト: testing パッケージを使用してテストを記述します。

    • func TestXxx(t *testing.T): テスト関数は Test で始まり、*testing.T 型の引数を取ります。
    • t.Fatalf(...): テストを失敗させ、メッセージを出力してテストの実行を停止します。
    • t.Error(...): テストを失敗させ、メッセージを出力しますが、テストの実行は継続します。
    • t.Logf(...): テストのログにメッセージを出力します。テストが成功した場合でも出力されます。
  4. Flaky Test (不安定なテスト): 前述の通り、実行するたびに結果が変わるテストのことです。時間依存のテスト、並行処理のテスト、外部サービスに依存するテストなどで発生しやすいです。これを解決するためには、テストのロジックをより堅牢にするか、テスト環境の変動を吸収するメカニズムを導入する必要があります。

技術的詳細

元の TestReset 関数は、固定された Duration 値(例: 100 * Millisecond)を使用してタイマーの動作を検証していました。このアプローチは、システムクロックの粒度やスケジューリングの変動に対して脆弱でした。例えば、Sleep(50 * Millisecond) の後に Reset(150 * Millisecond) を呼び出し、その後 Sleep(100 * Millisecond) を行うというシーケンスでは、厳密なタイミングが要求されます。もし Sleep がわずかに長くかかったり、タイマーの発火が遅れたりすると、テストが意図せず失敗する可能性がありました。

新しいアプローチでは、この不安定さを解消するために以下の変更が導入されました。

  1. testReset ヘルパー関数の導入:

    • 元の TestReset のロジックを testReset(d Duration) error というヘルパー関数に切り出しました。
    • このヘルパー関数は、テストで使用する基本となる時間単位 d を引数として受け取ります。
    • タイマーの期間やスリープ時間は、この d の倍数として定義されます(例: 2 * d, 3 * d)。これにより、テストの実行時間を柔軟に調整できるようになります。
    • テストが失敗した場合、t.Fatalf の代わりに errors.New を使用してエラーを返します。これにより、呼び出し元の TestReset 関数がエラーを捕捉し、再試行のロジックを実装できるようになります。
  2. 複数回の試行による堅牢化:

    • 新しい TestReset 関数は、testReset ヘルパー関数を異なる Duration 値で複数回試行するロジックを導入しました。
    • tries := []Duration{1 * unit, 3 * unit, 7 * unit, 15 * unit} という配列が定義されています。unit25 * Millisecond です。
    • テストは、最も短い Duration (25ms) から順に testReset を実行します。
    • もし testReset がエラーなく成功した場合、その Duration でテストがパスしたと判断し、それ以上の試行は行いません。
    • これにより、高速なマシンでは短い Duration でテストがすぐに完了し、低速なマシンや高負荷な環境ではより長い Duration を試すことで、テストが安定してパスする可能性が高まります。これは、テストが「遅い、負荷の高いハードウェアでも不安定にならないように、しかし高速なマシンでは不必要に遅くならないように」という設計思想に基づいています。
    • すべての Duration で試行してもテストが成功しなかった場合のみ、最終的に t.Error(err) を呼び出してテストを失敗させます。

この変更により、TestReset は環境の変動に対してより耐性を持つようになり、Goのテストスイート全体の信頼性向上に貢献しています。

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

--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -247,26 +247,49 @@ func TestSleepZeroDeadlock(t *testing.T) {
 	<-c
 }
 
-func TestReset(t *testing.T) {
-	t0 := NewTimer(100 * Millisecond)
-	Sleep(50 * Millisecond)
-	if t0.Reset(150*Millisecond) != true {
-		t.Fatalf("resetting unfired timer returned false")
+func testReset(d Duration) error {
+	t0 := NewTimer(2 * d)
+	Sleep(d)
+	if t0.Reset(3*d) != true {
+		return errors.New("resetting unfired timer returned false")
 	}
-	Sleep(100 * Millisecond)
+	Sleep(2 * d)
 	select {
 	case <-t0.C:
-		t.Fatalf("timer fired early")
+		return errors.New("timer fired early")
 	default:
 	}
-	Sleep(100 * Millisecond)
+	Sleep(2 * d)
 	select {
 	case <-t0.C:
 	default:
-		t.Fatalf("reset timer did not fire")
+		return errors.New("reset timer did not fire")
 	}
 
 	if t0.Reset(50*Millisecond) != false {
-		t.Fatalf("resetting expired timer returned true")
+		return errors.New("resetting expired timer returned true")
+	}
+	return nil
+}
+
+func TestReset(t *testing.T) {
+	// We try to run this test with increasingly larger multiples
+	// until one works so slow, loaded hardware isn't as flaky,
+	// but without slowing down fast machines unnecessarily.
+	const unit = 25 * Millisecond
+	tries := []Duration{
+		1 * unit,
+		3 * unit,
+		7 * unit,
+		15 * unit,
+	}
+	var err error
+	for _, d := range tries {
+		err = testReset(d)
+		if err == nil {
+			t.Logf("passed using duration %v", d)
+			return
+		}
 	}
+	t.Error(err)
 }

コアとなるコードの解説

testReset(d Duration) error 関数

この関数は、TestReset の主要なテストロジックをカプセル化しています。引数 d は、テストで使用される時間単位の基準となります。

  1. t0 := NewTimer(2 * d): 2 * d の期間で新しいタイマー t0 を作成します。
  2. Sleep(d): d の期間だけスリープします。この時点でタイマーはまだ発火していないはずです。
  3. if t0.Reset(3*d) != true: タイマーを 3 * d の期間でリセットします。タイマーがまだ発火していないため、Resettrue を返すはずです。もし false を返した場合、エラーを返します。
  4. Sleep(2 * d): さらに 2 * d だけスリープします。この時点で、リセットされたタイマーはまだ発火していないはずです(合計 d + 2*d = 3*d のスリープで、リセット後の期間 3*d に達する)。
  5. select { case <-t0.C: ... default: }:
    • case <-t0.C:: もしタイマーがこの時点で発火していたら(つまり、3*d よりも早く発火したら)、"timer fired early" というエラーを返します。
    • default:: タイマーがまだ発火していないことを確認します。
  6. Sleep(2 * d): さらに 2 * d だけスリープします。この時点で、リセットされたタイマーは発火しているはずです(合計 d + 2*d + 2*d = 5*d のスリープで、リセット後の期間 3*d を超えている)。
  7. select { case <-t0.C: ... default: }:
    • case <-t0.C:: タイマーが発火したことを確認します。
    • default:: もしタイマーが発火していなかったら、"reset timer did not fire" というエラーを返します。
  8. if t0.Reset(50*Millisecond) != false: 既に発火したタイマーをリセットしようとします。この場合、Resetfalse を返すはずです。もし true を返した場合、"resetting expired timer returned true" というエラーを返します。
  9. return nil: すべてのチェックが成功した場合、nil を返して成功を示します。

TestReset(t *testing.T) 関数

この関数は、testReset ヘルパー関数を呼び出し、テストの堅牢性を高めるためのロジックを含んでいます。

  1. const unit = 25 * Millisecond: 基本となる時間単位を 25ミリ秒 と定義します。
  2. tries := []Duration{1 * unit, 3 * unit, 7 * unit, 15 * unit}: testReset を試行する異なる時間単位の倍数を定義します。これにより、25ms, 75ms, 175ms, 375ms の順でテストが試行されます。
  3. var err error: testReset から返されるエラーを保持するための変数です。
  4. for _, d := range tries: tries 配列の各 Duration d に対してループを実行します。
  5. err = testReset(d): testReset ヘルパー関数を現在の d で呼び出します。
  6. if err == nil: もし testReset がエラーなく成功した場合(nil を返した場合)、
    • t.Logf("passed using duration %v", d): どの Duration でテストがパスしたかをログに出力します。
    • return: テストを終了し、成功とします。
  7. t.Error(err): ループが終了しても testReset が一度も成功しなかった場合(つまり、すべての Duration でエラーが返された場合)、最後に発生したエラーを t.Error で出力し、テストを失敗とします。

この構造により、テストはまず短い時間間隔で迅速に実行を試み、もしそれが不安定な環境で失敗した場合には、より長い時間間隔で再試行することで、テストの信頼性を大幅に向上させています。

関連リンク

  • Go Issue #4690: https://code.google.com/p/go/issues/detail?id=4690 (古いGoogle Codeのリンクですが、コミットメッセージに記載されています)
  • Gerrit Change-Id: https://golang.org/cl/7181052 (GoのコードレビューシステムGerritのリンク)

参考にした情報源リンク