[インデックス 12286] ファイルの概要
このコミットは、Go言語の標準ライブラリであるtime
パッケージ内のsleep_test.go
ファイルに対して行われたものです。具体的には、TestAfterTick
というテスト関数において、time.Sleep
の実行時間が期待される上限を超過した場合のテスト失敗条件を、go test -short
モードで実行する際にはスキップするように変更しています。これにより、仮想化環境や高負荷なマシンで発生しやすかった、時間計測の不安定さに起因するテストの不安定性(flakiness)を解消することを目的としています。
コミット
commit 8c5290502fc1d7cddf416614aab5d2ad3c1b9b08
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 29 13:14:05 2012 -0800
time: skip a often-flaky test in short mode
In -test.short mode, skip measuring the upper bound of time
sleeps. The API only guarantees minimum bounds on sleeps,
anyway, so this isn't a bug we're ignoring as much as it is
simply observing bad builder virtualization and/or loaded
machines.
We keep the test in full mode where developers will
presumably be running on a lightly-loaded, native, fast
machine.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5713044
---\n src/pkg/time/sleep_test.go | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/pkg/time/sleep_test.go b/src/pkg/time/sleep_test.go
index 9b0b7f7e06..526d58d75e 100644
--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -120,8 +120,11 @@ func TestAfterTick(t *testing.T) {
t1 := Now()
d := t1.Sub(t0)
target := Delta * Count
- if d < target*9/10 || d > target*30/10 {
- t.Fatalf("%d ticks of %s took %s, expected %s", Count, Delta, d, target)
+ if d < target*9/10 {
+ t.Fatalf("%d ticks of %s too fast: took %s, expected %s", Count, Delta, d, target)
+ }
+ if !testing.Short() && d > target*30/10 {
+ t.Fatalf("%d ticks of %s too slow: took %s, expected %s", Count, Delta, d, target)
}
}
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8c5290502fc1d7cddf416614aab5d2ad3c1b9b08
元コミット内容
time: skip a often-flaky test in short mode
In -test.short mode, skip measuring the upper bound of time
sleeps. The API only guarantees minimum bounds on sleeps,
anyway, so this isn't a bug we're ignoring as much as it is
simply observing bad builder virtualization and/or loaded
machines.
We keep the test in full mode where developers will
presumably be running on a lightly-loaded, native, fast
machine.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5713044
変更の背景
この変更の背景には、Go言語のテストスイートがCI/CD環境(特にGoの公式ビルダ)で実行される際に、time.Sleep
の挙動に起因する不安定なテスト(flaky test)が発生していたという問題があります。
time.Sleep
関数は、指定された期間だけ現在のゴルーチンをスリープさせますが、その実装はOSのスケジューラに依存します。OSのスケジューラは、他のプロセスやシステム負荷、仮想化環境のオーバーヘッドなど、様々な要因によってスリープの精度に影響を与える可能性があります。特に、指定された期間よりも「長く」スリープしてしまうことは、高負荷なシステムや仮想化された環境では頻繁に発生し得ます。
元のテストコードでは、time.Sleep
の実行時間が期待される「下限」だけでなく、「上限」も厳密にチェックしていました。しかし、time
パッケージのAPIは、スリープの「最小保証」しか提供しておらず、指定された時間よりも長くスリープしないことを保証していません。そのため、システム負荷や仮想化環境の遅延によってスリープ時間がわずかに長くなっただけでテストが失敗するという、本来の機能とは関係のない不安定な挙動が見られました。
このような不安定なテストは、開発者の生産性を低下させ、CI/CDパイプラインの信頼性を損ないます。テストが失敗しても、それが実際のバグによるものなのか、環境的な要因によるものなのかを判断する手間が発生するためです。
このコミットは、この問題を解決するために、go test -short
モードでテストを実行する際に、スリープ時間の上限チェックをスキップするように変更しました。これにより、開発者がローカルでフルテストを実行する際には厳密なチェックを維持しつつ、CI環境やクイックテストの際には不必要な失敗を避けることができます。
前提知識の解説
1. go test -short
モード
Go言語の標準テストツールであるgo test
には、-short
というフラグがあります。このフラグを付けてテストを実行すると、テストコード内でtesting.Short()
関数がtrue
を返すようになります。
testing.Short()
は、テストの実行時間を短縮したい場合や、リソースを大量に消費するテスト、外部サービスに依存するテストなどを、通常の開発サイクルではスキップしたい場合に利用されます。開発者は、この関数を使って、テストの一部を条件付きで実行するかどうかを制御できます。
例えば、以下のように使用されます。
func TestSomethingLong(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
// 時間のかかるテストロジック
}
これにより、go test
と実行した場合はTestSomethingLong
が実行されますが、go test -short
と実行した場合はスキップされます。
2. time.Sleep
の保証
Go言語のtime
パッケージに含まれるtime.Sleep(d Duration)
関数は、指定された期間d
だけ現在のゴルーチンをスリープさせます。しかし、この関数が保証するのは「少なくともd
の期間はスリープする」という「最小保証」です。
つまり、time.Sleep(1 * time.Second)
と呼び出した場合、ゴルーチンは少なくとも1秒間はスリープしますが、システム負荷やOSのスケジューリングの都合により、1秒よりも長く(例えば1.1秒や1.5秒)スリープする可能性があります。これは、time.Sleep
がOSのタイマー機能に依存しており、OSが他のタスクの実行や割り込み処理などを行うため、厳密な精度を保証できないことに起因します。
この「最小保証」という特性は、多くのプログラミング言語やOSにおけるスリープ関数の一般的な挙動です。そのため、スリープ時間の上限を厳密にチェックするテストは、環境によっては不安定になりやすいという問題があります。
3. Flaky Test (不安定なテスト)
Flaky testとは、コードの変更がないにもかかわらず、実行するたびに成功したり失敗したりするテストのことです。このようなテストは、以下のような様々な要因によって引き起こされます。
- 並行処理の競合状態 (Race Conditions): 複数のゴルーチンやスレッドが共有リソースにアクセスする順序が不定な場合。
- 時間依存性 (Time Dependencies): テストが特定の時間内に完了することを期待しているが、システム負荷やスケジューリングによって時間が変動する場合(今回のケース)。
- 外部依存性 (External Dependencies): データベース、ネットワークサービス、ファイルシステムなど、テストが依存する外部リソースの可用性やパフォーマンスが不安定な場合。
- 環境依存性 (Environment Dependencies): テストが実行される環境(OS、ハードウェア、仮想化設定など)によって挙動が変わる場合。
Flaky testは、CI/CDパイプラインの信頼性を低下させ、開発者がテスト結果を信用できなくなる原因となります。真のバグを見逃したり、存在しないバグを修正しようと無駄な時間を費やしたりする可能性があります。
技術的詳細
このコミットは、time.Sleep
の特性とflaky testの問題を考慮し、TestAfterTick
テストのロジックを修正しています。
元のコードでは、TestAfterTick
はtime.Tick
(内部でtime.Sleep
を使用)が生成するイベントの総時間を計測し、その時間がtarget
(期待される合計時間)の90%から300%の範囲内にあることを期待していました。
// 元のコード
if d < target*9/10 || d > target*30/10 {
t.Fatalf("%d ticks of %s took %s, expected %s", Count, Delta, d, target)
}
この条件は、以下の2つの部分から構成されています。
d < target*9/10
: 実行時間が期待値の90%未満である場合(早すぎる場合)d > target*30/10
: 実行時間が期待値の300%を超える場合(遅すぎる場合)
time.Sleep
が「最小保証」しかしないという性質上、実行時間がtarget*9/10
より短くなることは、通常は問題のある挙動(例えば、スリープが正しく機能していない)を示します。しかし、d > target*30/10
という条件は、システム負荷や仮想化環境の遅延によって容易に満たされてしまう可能性があります。これは、time.Sleep
のAPIが保証しない範囲の挙動をテストしていることになります。
このコミットでは、この「遅すぎる場合」のチェックをtesting.Short()
の条件付きで実行するように変更しました。
// 変更後のコード
if d < target*9/10 {
t.Fatalf("%d ticks of %s too fast: took %s, expected %s", Count, Delta, d, target)
}
if !testing.Short() && d > target*30/10 {
t.Fatalf("%d ticks of %s too slow: took %s, expected %s", Count, Delta, d, target)
}
この変更により、以下の挙動が実現されます。
go test
(フルモード):testing.Short()
はfalse
を返すため、!testing.Short()
はtrue
となり、両方の条件(早すぎる場合と遅すぎる場合)がチェックされます。これは、開発者がローカルの安定した環境でテストを実行する際に、より厳密な時間計測のチェックを維持することを意図しています。go test -short
(ショートモード):testing.Short()
はtrue
を返すため、!testing.Short()
はfalse
となり、d > target*30/10
のチェックはスキップされます。これにより、CI環境や高負荷なマシンでの実行時に、不必要なテスト失敗を避けることができます。
このアプローチは、テストの目的とAPIの保証範囲を整合させるための実用的な解決策です。time.Sleep
のAPIが最小保証しかしない以上、上限の厳密なチェックは環境依存のflakinessを引き起こす可能性が高いため、それを条件付きでスキップすることは妥当な判断と言えます。これにより、テストスイート全体の信頼性が向上し、開発者は真のバグに集中できるようになります。
コアとなるコードの変更箇所
src/pkg/time/sleep_test.go
ファイルのTestAfterTick
関数内の条件分岐が変更されています。
--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -120,8 +120,11 @@ func TestAfterTick(t *testing.T) {
t1 := Now()
d := t1.Sub(t0)
target := Delta * Count
- if d < target*9/10 || d > target*30/10 {
- t.Fatalf("%d ticks of %s took %s, expected %s", Count, Delta, d, target)
+ if d < target*9/10 {
+ t.Fatalf("%d ticks of %s too fast: took %s, expected %s", Count, Delta, d, target)
+ }
+ if !testing.Short() && d > target*30/10 {
+ t.Fatalf("%d ticks of %s too slow: took %s, expected %s", Count, Delta, d, target)
}
}
コアとなるコードの解説
変更前は、if d < target*9/10 || d > target*30/10
という単一の条件文で、スリープ時間が早すぎる場合と遅すぎる場合の両方をチェックしていました。
変更後は、この単一の条件文が2つの独立したif
文に分割されました。
-
if d < target*9/10 { ... }
- この条件は、計測された時間
d
が期待される合計時間target
の90%未満である場合にトリガーされます。これは、スリープが期待よりも「早すぎる」ことを意味し、通常は問題のある挙動(例えば、スリープが全く機能していない、または非常に短い時間しかスリープしていない)を示します。このチェックは、testing.Short()
モードに関わらず常に実行されます。
- この条件は、計測された時間
-
if !testing.Short() && d > target*30/10 { ... }
- この条件は、
testing.Short()
がfalse
(つまり、-short
フラグなしでテストが実行されている)であり、かつ計測された時間d
がtarget
の300%を超える場合にトリガーされます。これは、スリープが期待よりも「遅すぎる」ことを意味します。 !testing.Short()
という条件が追加されたことで、go test -short
モードで実行された場合は、この「遅すぎる」ことによるテスト失敗のチェックがスキップされます。これにより、高負荷な環境や仮想化環境での不必要なテスト失敗を防ぎます。
- この条件は、
この変更により、テストの厳密性と実用性のバランスが取られ、time.Sleep
のAPI保証範囲に合わせたより堅牢なテストスイートが実現されています。
関連リンク
- Go言語の
testing
パッケージドキュメント: https://pkg.go.dev/testing - Go言語の
time
パッケージドキュメント: https://pkg.go.dev/time - Goの公式Issueトラッカー (関連する議論が見つかる可能性): https://github.com/golang/go/issues
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージにある
https://golang.org/cl/5713044
はこのGerritの変更リストへのリンクです)
参考にした情報源リンク
- Go言語の公式ドキュメント (testing, timeパッケージ)
- Go言語のテストに関する一般的なプラクティスやflaky testに関する記事 (一般的な知識として)
- GitHubのコミットページ: https://github.com/golang/go/commit/8c5290502fc1d7cddf416614aab5d2ad3c1b9b08
- GoのGerrit変更リスト: https://golang.org/cl/5713044 (現在はGitHubにリダイレクトされる)