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

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

このコミットは、Go言語の実験的な型システム (exp/types) のテストコード (gcimporter_test.go) における改善です。具体的には、テスト実行後に生成される一時ファイルを適切にクリーンアップするよう修正されています。これにより、テストの実行環境が汚染されるのを防ぎ、テストの信頼性と再現性を向上させています。コミットメッセージにある "Fixes #3739" は、テスト実行後に一時ファイルが残存するという問題(またはそれに類する問題)を解決することを示唆しています。

コミット

  • コミットハッシュ: 985261429162edc07e0e97741f425c5aded55641
  • Author: Shenghou Ma minux.ma@gmail.com
  • Date: Fri Jun 15 02:52:18 2012 +0800
  • コミットメッセージ:
    exp/types: clean up objects after test
            Fixes #3739.
    
    R=bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/6295083
    

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

https://github.com/golang/go/commit/985261429162edc07e0e97741f425c5aded55641

元コミット内容

exp/types: clean up objects after test
        Fixes #3739.

R=bradfitz, rsc
CC=golang-dev
https://golang.org/cl/6295083

変更の背景

この変更の主な背景は、テスト実行によって生成される一時ファイルが、テスト終了後もシステム上に残存してしまう問題に対処することです。テストは、その性質上、特定の操作を実行し、その結果を検証するために一時的なリソース(ファイル、ディレクトリ、ネットワーク接続など)を作成することがよくあります。これらのリソースがテスト終了後に適切にクリーンアップされないと、以下のような問題が発生します。

  1. 環境の汚染: テストが実行されるたびに不要なファイルが蓄積され、ディスクスペースを消費したり、後続のテストや他の開発作業に影響を与えたりする可能性があります。
  2. テストの再現性の低下: 残存する一時ファイルが、後続のテスト実行に予期せぬ影響を与え、テスト結果が不安定になる(「flaky test」と呼ばれる)原因となることがあります。例えば、前回のテストで生成されたファイルが残っているために、今回のテストが異なる振る舞いをする、といった状況です。
  3. デバッグの困難さ: テストが失敗した際に、残存する一時ファイルが原因である場合、その特定とデバッグが困難になります。

コミットメッセージにある "Fixes #3739" は、この問題がGoの公式イシュートラッカーで報告された具体的な問題(または、その問題に関連する内部的な参照番号)であることを示唆しています。Web検索の結果では、Goの公式イシュートラッカーに直接「Issue #3739」という形式のイシューは確認できませんでしたが、Goのランタイムやガベージコレクションに関連する議論で「3739」という数字がコードの行番号として参照されるケースが見られました。これは、テストにおけるリソース管理、特に一時ファイルのクリーンアップが、システム全体の安定性やテストの健全性にとって重要であることを示しています。

このコミットは、exp/typesパッケージのテストにおいて、コンパイルによって生成される中間ファイル(オブジェクトファイル)がテスト後に確実に削除されるようにすることで、上記の課題を解決しようとしています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびテストに関する基本的な知識が必要です。

  1. Go言語のexp/typesパッケージ: exp/typesは、Go言語の型システムを扱うための実験的なパッケージです。Goのコンパイラやツールが型情報をどのように扱うかを理解する上で重要です。このパッケージは、Goの型チェッカーや、Goのソースコードから型情報を抽出するツールなどで利用されます。テストコード (gcimporter_test.go) は、このパッケージの機能、特にGoコンパイラが生成する型情報(GCインポートデータ)のインポート機能をテストしています。

  2. Goのテストフレームワーク (testingパッケージ): Goには標準ライブラリとしてtestingパッケージが提供されており、これを用いてユニットテストやベンチマークテストを記述します。

    • func TestXxx(t *testing.T): テスト関数はTestで始まり、*testing.T型の引数を取ります。
    • t.Errorf(...), t.Fatalf(...): テスト中にエラーが発生した場合に呼び出すメソッドです。t.Fatalfはエラーを報告した後にテストの実行を停止します。
    • t.Logf(...): テストのログを出力します。
  3. osパッケージとファイル操作:

    • os.Remove(name string): 指定されたパスのファイルまたは空のディレクトリを削除します。
    • filepath.Join(elem ...string): 複数のパス要素を結合して、プラットフォーム固有のパス区切り文字で区切られた単一のパスを生成します。
  4. execパッケージと外部コマンドの実行:

    • exec.Command(name string, arg ...string): 指定されたコマンドと引数を持つCmd構造体を作成します。
    • cmd.Dir: コマンドを実行する作業ディレクトリを設定します。
    • cmd.CombinedOutput(): コマンドを実行し、その標準出力と標準エラー出力を結合したバイトスライスとして返します。
    • err != nil: コマンドの実行中にエラーが発生したかどうかをチェックします。
  5. deferステートメント: Goのdeferステートメントは、その関数がリターンする直前に、指定された関数呼び出しを延期します。これは、リソースのクリーンアップ(ファイルのクローズ、ロックの解除など)を確実に行うための非常に便利な機能です。deferされた関数は、たとえエラーが発生して関数が途中で終了した場合でも実行されます。

  6. Goのビルドプロセスと中間ファイル: Goのコンパイラ(gc)は、ソースコードをコンパイルする際に、中間ファイル(オブジェクトファイルなど)を生成することがあります。これらのファイルは通常、build.ToolDirで指定される一時的なディレクトリに配置されます。テストでは、これらのコンパイル済みファイルがテスト対象の機能(この場合は型情報のインポート)の入力として使用されます。

  7. build.ToolDirbuild.ArchChar:

    • build.ToolDir: Goのビルドツールが配置されているディレクトリのパスを提供します。コンパイラ(gc)のパスを特定するために使用されます。
    • build.ArchChar(goarch string): 指定されたアーキテクチャ(例: amd64)に対応する文字(例: 6)を返します。Goのコンパイラは、生成するオブジェクトファイル名にこのアーキテクチャ文字を含めることがあります(例: exports.6)。

技術的詳細

このコミットの技術的な詳細は、主にcompile関数の変更と、その呼び出し元でのdefer os.Removeの導入にあります。

compile関数の変更点

変更前:

func compile(t *testing.T, dirname, filename string) {
	cmd := exec.Command(gcPath, filename)
	cmd.Dir = dirname
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Errorf("%s %s failed: %s", gcPath, filename, err)
		return
	}
	t.Logf("%s", string(out))
}

変更後:

func compile(t *testing.T, dirname, filename string) (outFn string) {
	cmd := exec.Command(gcPath, filename)
	cmd.Dir = dirname
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("%s %s failed: %s", gcPath, filename, err)
		return ""
	}
	t.Logf("%s", string(out))
	archCh, _ := build.ArchChar(runtime.GOARCH)
	// filename should end with ".go"
	return filepath.Join(dirname, filename[:len(filename)-2]+archCh)
}

主な変更点は以下の通りです。

  1. 戻り値の追加: compile関数がoutFn stringという戻り値を持つようになりました。このoutFnは、コンパイルによって生成される出力ファイル(オブジェクトファイル)のパスを返します。
  2. エラーハンドリングの変更: t.Errorft.Fatalfに変更されました。これにより、コンパイルが失敗した場合は、その場でテストが終了するようになります。これは、コンパイルが成功しない限り、後続のテストステップが無意味になるため、より適切なエラーハンドリングと言えます。
  3. 出力ファイルパスの生成:
    • archCh, _ := build.ArchChar(runtime.GOARCH): 現在の実行環境のアーキテクチャに対応する文字(例: amd64なら6)を取得します。
    • return filepath.Join(dirname, filename[:len(filename)-2]+archCh): コンパイルされたオブジェクトファイルのパスを構築して返します。Goのコンパイラは、通常、ソースファイル名(例: exports.go)から.go拡張子を取り除き、代わりにアーキテクチャ文字を付加した名前(例: exports.6)でオブジェクトファイルを生成します。filepath.Joinは、ディレクトリ名とファイル名を結合し、適切なパスを生成します。

TestGcImport関数でのdefer os.Removeの導入

変更前:

	compile(t, "testdata", "exports.go")

	nimports := 0
	if testPath(t, "./testdata/exports") {

変更後:

	if outFn := compile(t, "testdata", "exports.go"); outFn != "" {
		defer os.Remove(outFn)
	}

	nimports := 0
	if testPath(t, "./testdata/exports") {

この変更は、compile関数が返す出力ファイルパスを利用して、テスト終了時にそのファイルを削除することを保証します。

  1. if outFn := compile(t, "testdata", "exports.go"); outFn != ""
    • compile関数を呼び出し、その戻り値(生成されたオブジェクトファイルのパス)をoutFn変数に代入します。
    • outFn != ""という条件は、コンパイルが成功し、有効な出力ファイルパスが返された場合にのみ、後続のクリーンアップ処理を行うことを保証します。compile関数がt.Fatalfで終了した場合、outFnは空文字列になるため、os.Removeは呼び出されません。
  2. defer os.Remove(outFn)
    • これがこのコミットの核心です。deferキーワードにより、os.Remove(outFn)は、現在のTestGcImport関数が終了する直前(正常終了でもパニックでも)に実行されることが保証されます。
    • これにより、exports.goのコンパイルによって生成された一時ファイル(例: testdata/exports.6)が、テストの実行が完了した後に自動的に削除されます。

この一連の変更により、テストの実行がよりクリーンになり、一時ファイルがシステムに残存することによる問題が解消されます。

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

--- a/src/pkg/exp/types/gcimporter_test.go
+++ b/src/pkg/exp/types/gcimporter_test.go
@@ -36,15 +36,18 @@ func init() {
 	gcPath = filepath.Join(build.ToolDir, gc)
 }
 
-func compile(t *testing.T, dirname, filename string) {
+func compile(t *testing.T, dirname, filename string) (outFn string) {
 	cmd := exec.Command(gcPath, filename)
 	cmd.Dir = dirname
 	out, err := cmd.CombinedOutput()
 	if err != nil {
-		t.Errorf("%s %s failed: %s", gcPath, filename, err)
-		return
+		t.Fatalf("%s %s failed: %s", gcPath, filename, err)
+		return ""
 	}
 	t.Logf("%s", string(out))
+	archCh, _ := build.ArchChar(runtime.GOARCH)
+	// filename should end with ".go"
+	return filepath.Join(dirname, filename[:len(filename)-2]+archCh)
 }
 
 // Use the same global imports map for all tests. The effect is
@@ -99,7 +102,9 @@ func TestGcImport(t *testing.T) {
 		return
 	}
 
-	compile(t, "testdata", "exports.go")
+	if outFn := compile(t, "testdata", "exports.go"); outFn != "" {
+		defer os.Remove(outFn)
+	}
 
 	nimports := 0
 	if testPath(t, "./testdata/exports") {

コアとなるコードの解説

func compile の変更

-func compile(t *testing.T, dirname, filename string) {
+func compile(t *testing.T, dirname, filename string) (outFn string) {
  • compile関数のシグネチャが変更され、outFn stringという名前付き戻り値が追加されました。これにより、この関数がコンパイルによって生成される出力ファイルのパスを返すことができるようになります。
 	if err != nil {
-		t.Errorf("%s %s failed: %s", gcPath, filename, err)
-		return
+		t.Fatalf("%s %s failed: %s", gcPath, filename, err)
+		return ""
 	}
  • コンパイルコマンドの実行が失敗した場合のエラーハンドリングがt.Errorfからt.Fatalfに変更されました。t.Fatalfはエラーを報告した後にテストの実行を即座に停止します。これにより、コンパイルが失敗した状態で後続のテストステップが実行されるのを防ぎます。また、t.Fatalfが呼び出された場合、関数はそこで終了するため、outFnにはデフォルト値である空文字列が返されます。
+	archCh, _ := build.ArchChar(runtime.GOARCH)
+	// filename should end with ".go"
+	return filepath.Join(dirname, filename[:len(filename)-2]+archCh)
  • コンパイルが成功した場合、この行が実行され、生成されたオブジェクトファイルのパスが構築されて返されます。
    • build.ArchChar(runtime.GOARCH): 現在のシステムアーキテクチャ(例: amd64)に対応する文字(例: 6)を取得します。Goコンパイラは、生成するオブジェクトファイル名にこの文字を含める慣習があります。
    • filename[:len(filename)-2]: 入力ファイル名(例: exports.go)から最後の2文字(.go)を削除します。
    • +archCh: 削除した拡張子の代わりに、取得したアーキテクチャ文字を付加します(例: exports + 6 = exports6)。
    • filepath.Join(dirname, ...): ディレクトリ名(testdata)と構築されたファイル名(exports6)を結合し、完全なパス(例: testdata/exports6)を生成します。このパスがcompile関数の戻り値として返されます。

func TestGcImport の変更

-	compile(t, "testdata", "exports.go")
+	if outFn := compile(t, "testdata", "exports.go"); outFn != "" {
+		defer os.Remove(outFn)
+	}
  • compile関数の呼び出し方が変更されました。
  • outFn := compile(t, "testdata", "exports.go"): compile関数を呼び出し、その戻り値(生成されたオブジェクトファイルのパス)を新しい変数outFnに代入します。
  • if outFn != "": compile関数が正常に実行され、有効な出力ファイルパスが返された場合にのみ、以下のクリーンアップ処理を実行します。これは、コンパイルが失敗してoutFnが空文字列の場合に、存在しないファイルを削除しようとするエラーを防ぐためです。
  • defer os.Remove(outFn): この行がこのコミットの最も重要な部分です。deferキーワードにより、os.Remove(outFn)は、TestGcImport関数が終了する直前(正常終了、エラーによる終了、パニックによる終了のいずれの場合でも)に実行されることが保証されます。これにより、テスト中に生成された一時ファイルが、テスト終了後に確実に削除され、テスト環境がクリーンに保たれます。

関連リンク

参考にした情報源リンク