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

[インデックス 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 が共有リソースであったことです。

  1. 並行テスト実行: 複数のテストが同時に実行される場合、あるテストが testGIF を変更している最中に、別のテストがその変更された testGIF を読み込んでしまう可能性があります。これにより、テストの期待される結果が非決定論的になります。
  2. 連続テスト実行: 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)
  1. gif := make([]byte, len(testGIF)): これは、testGIF と同じ長さの新しいバイトスライス gif を作成します。make 関数は、スライス、マップ、チャネルなどの組み込み型を初期化するために使用されます。ここでは、len(testGIF)testGIF の長さを取得し、その長さのバイトスライスをゼロ値(バイトの場合は0)で初期化します。
  2. copy(gif, testGIF): これは、testGIF の内容を新しく作成した gif スライスにコピーします。copy 関数は、ソーススライスからデスティネーションスライスに要素をコピーします。これにより、giftestGIF の独立したコピーとなり、以降の操作で gif を変更しても testGIF には影響が及びません。

この変更により、TestBounds 関数内で testGIF を直接操作していた箇所がすべて gif を操作するように変更されています。例えば、testGIF[32] = 2gif[32] = 2 に、try(t, testGIF, want)try(t, gif, want) に置き換えられています。

この修正によって、TestBounds が実行されるたびに、testGIF の初期状態が常に保証されるようになります。これにより、テストの実行順序や並行実行の有無に関わらず、テストが常に同じ結果を返すようになり、go test -cpu=1,1 のような特定のテスト実行シナリオでの非決定論的な失敗が解消されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (特に image/gif パッケージのテストファイル)
  • Go言語のテストに関する一般的なプラクティスとベストプラクティス
  • Go言語の make および copy 組み込み関数のドキュメント
  • Go言語のテストにおける並行実行と共有状態に関する議論