[インデックス 15944] ファイルの概要
このコミットは、Go言語の標準ライブラリである image/gif
パッケージ内のテストファイル reader_test.go
に加えられた変更を記録しています。具体的には、TestBounds
関数が修正され、テストの再現性が向上しました。
コミット
commit adb9d60cd1f6ff88628bbe6124969faa4f51d346
Author: Dave Cheney <dave@cheney.net>
Date: Tue Mar 26 16:20:17 2013 +1100
image/gif: make test repeatable
Fixes issue with go test -cpu=1,1
R=minux.ma, bradfitz, nigeltao
CC=golang-dev
https://golang.org/cl/7808045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/adb9d60cd1f6ff88628bbe6124969faa4f51d346
元コミット内容
image/gif: make test repeatable
Fixes issue with go test -cpu=1,1
このコミットは、image/gif
パッケージのテストが再現可能になるように修正するものです。特に、go test -cpu=1,1
オプションを指定してテストを実行した際に発生する問題を解決します。
変更の背景
Go言語のテストフレームワークは、デフォルトでテストを並行して実行する能力を持っています。これは、テストスイート全体の実行時間を短縮するのに役立ちます。しかし、テストがグローバルな状態や共有リソースを不適切に操作する場合、並行実行によってテストの失敗が非決定論的になる(つまり、特定の条件下でのみ失敗する)ことがあります。
このコミットの背景には、image/gif
パッケージの reader_test.go
内の TestBounds
テストが、このような非決定論的な振る舞いを示していたという問題があります。コミットメッセージにある go test -cpu=1,1
は、Goのテストコマンドにおいて、テストを並行実行する際のCPUの最大数を指定するフラグです。go test -cpu=1
はテストを単一のCPUコアで実行することを意味し、go test -cpu=1,1
はテストを並行実行せず、かつ単一のCPUコアに制限して実行することを意味します。このような特定の実行環境で問題が顕在化したということは、テストが共有される testGIF
変数を直接変更しており、その変更が他のテスト実行や、同じテストの複数回実行に影響を与えていた可能性が高いことを示唆しています。
テストの再現性(repeatability)は、ソフトウェア開発において極めて重要です。再現性のないテストは、バグの特定を困難にし、CI/CDパイプラインの信頼性を損ない、開発者の生産性を低下させます。このコミットは、このようなテストの不安定性を解消し、テストスイートの信頼性を高めることを目的としています。
前提知識の解説
- Go言語のテスト: Go言語には、標準でテストをサポートする
testing
パッケージが組み込まれています。テストファイルは通常、テスト対象のファイルと同じディレクトリに_test.go
サフィックスを付けて配置されます。go test
コマンドで実行され、並行テスト、ベンチマークテスト、カバレッジレポートなどの機能を提供します。 go test -cpu
フラグ:go test -cpu N
は、テストを並行実行する際に使用するCPUコアの最大数を指定します。N
が1の場合、テストは並行実行されません。N
がカンマ区切りで複数指定された場合(例:-cpu=1,2,4
)、テストはそれぞれのCPU数で複数回実行されます。このコミットで言及されている-cpu=1,1
は、テストを並行実行せず、かつ単一のCPUコアに制限して実行するシナリオで問題が顕在化したことを示しています。これは、テストがグローバルな状態を破壊し、その後のテスト実行に影響を与えるようなケースで発生しやすいです。- GIF (Graphics Interchange Format): GIFは、画像ファイル形式の一つで、特にアニメーションをサポートすることで知られています。このコミットが関連する
image/gif
パッケージは、Go言語でGIF画像を読み書きするための機能を提供します。 - 画像データ構造: GIF画像データは、ヘッダ、論理画面記述子、グローバルカラーテーブル、画像記述子、ローカルカラーテーブル、画像データ、拡張ブロック、トレーラなど、複数のセクションで構成されます。このコミットで変更されている
testGIF
は、おそらくテスト用にハードコードされたGIFバイト配列であり、その特定のオフセット(32
番目のバイト)が画像の境界情報(幅や高さなど)に関連するデータを含んでいると推測されます。 - テストの冪等性 (Idempotence): 冪等性とは、ある操作を複数回実行しても、1回実行した場合と同じ結果になる性質を指します。テストにおいては、テストケースが実行されるたびに、そのテストが外部の状態に依存せず、また外部の状態を変更しない(あるいは変更しても元に戻す)ことで、常に同じ結果を返すことが理想的です。このコミットは、テストの冪等性を確保するための修正です。
技術的詳細
このコミットの核心は、Go言語のテストにおける「共有状態」の問題を解決することにあります。reader_test.go
内の TestBounds
関数は、testGIF
というバイトスライス(おそらくパッケージレベルで定義されたグローバル変数、またはそれに準ずるもの)を直接変更していました。
テストが実行されるたびに testGIF
の特定のバイト(オフセット32番目とその周辺)を書き換えることで、GIF画像の境界情報を意図的に不正な値に設定し、image/gif
パッケージのデコーダが適切にエラーを検出するかどうかを検証していました。
問題は、testGIF
が共有リソースであったことです。
- 並行テスト実行: 複数のテストが同時に実行される場合、あるテストが
testGIF
を変更している最中に、別のテストがその変更されたtestGIF
を読み込んでしまう可能性があります。これにより、テストの期待される結果が非決定論的になります。 - 連続テスト実行:
go test -cpu=1,1
のように、テストが単一のCPUで連続して実行される場合でも、前のテスト実行がtestGIF
を変更したまま終了し、その変更が次のテスト実行に引き継がれてしまう可能性があります。これにより、テストが期待通りに動作しないことがあります。
このコミットの解決策は、テスト内で testGIF
の「ローカルコピー」を作成し、そのコピーに対して変更を加えることです。これにより、元の testGIF
は変更されず、各テスト実行が独立したデータセットで動作することが保証されます。これは、テストの分離性(isolation)を確保し、テストの再現性を高めるための標準的なプラクティスです。
具体的には、make([]byte, len(testGIF))
で testGIF
と同じサイズの新しいバイトスライス gif
を作成し、copy(gif, testGIF)
で testGIF
の内容を gif
にコピーしています。その後のテストロジックでは、testGIF
ではなく、このローカルコピーである gif
を操作しています。
コアとなるコードの変更箇所
変更は src/pkg/image/gif/reader_test.go
ファイルの TestBounds
関数内で行われています。
--- a/src/pkg/image/gif/reader_test.go
+++ b/src/pkg/image/gif/reader_test.go
@@ -114,22 +114,25 @@ func try(t *testing.T, b []byte, want string) {
}
func TestBounds(t *testing.T) {
+ // make a local copy of testGIF
+ gif := make([]byte, len(testGIF))
+ copy(gif, testGIF)
// Make the bounds too big, just by one.
- testGIF[32] = 2
+ gif[32] = 2
want := "gif: frame bounds larger than image bounds"
- try(t, testGIF, want)
+ try(t, gif, want)
// Make the bounds too small; does not trigger bounds
// check, but now there's too much data.
- testGIF[32] = 0
+ gif[32] = 0
want = "gif: too much image data"
- try(t, testGIF, want)
- testGIF[32] = 1
+ try(t, gif, want)
+ gif[32] = 1
// Make the bounds really big, expect an error.
want = "gif: frame bounds larger than image bounds"
for i := 0; i < 4; i++ {
- testGIF[32+i] = 0xff
+ gif[32+i] = 0xff
}\n- try(t, testGIF, want)\n+ try(t, gif, want)\n }
コアとなるコードの解説
変更の核心は、TestBounds
関数の冒頭に追加された以下の3行です。
// make a local copy of testGIF
gif := make([]byte, len(testGIF))
copy(gif, testGIF)
gif := make([]byte, len(testGIF))
: これは、testGIF
と同じ長さの新しいバイトスライスgif
を作成します。make
関数は、スライス、マップ、チャネルなどの組み込み型を初期化するために使用されます。ここでは、len(testGIF)
でtestGIF
の長さを取得し、その長さのバイトスライスをゼロ値(バイトの場合は0)で初期化します。copy(gif, testGIF)
: これは、testGIF
の内容を新しく作成したgif
スライスにコピーします。copy
関数は、ソーススライスからデスティネーションスライスに要素をコピーします。これにより、gif
はtestGIF
の独立したコピーとなり、以降の操作でgif
を変更してもtestGIF
には影響が及びません。
この変更により、TestBounds
関数内で testGIF
を直接操作していた箇所がすべて gif
を操作するように変更されています。例えば、testGIF[32] = 2
は gif[32] = 2
に、try(t, testGIF, want)
は try(t, gif, want)
に置き換えられています。
この修正によって、TestBounds
が実行されるたびに、testGIF
の初期状態が常に保証されるようになります。これにより、テストの実行順序や並行実行の有無に関わらず、テストが常に同じ結果を返すようになり、go test -cpu=1,1
のような特定のテスト実行シナリオでの非決定論的な失敗が解消されます。
関連リンク
- Go言語の
testing
パッケージ: https://pkg.go.dev/testing - Go言語の
image/gif
パッケージ: https://pkg.go.dev/image/gif - Go言語の
go test
コマンドに関するドキュメント: https://pkg.go.dev/cmd/go#hdr-Test_packages
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
image/gif
パッケージのテストファイル) - Go言語のテストに関する一般的なプラクティスとベストプラクティス
- Go言語の
make
およびcopy
組み込み関数のドキュメント - Go言語のテストにおける並行実行と共有状態に関する議論