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

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

このコミットは、Go言語の標準ライブラリosパッケージ内のテストコードにおける一時ディレクトリの管理方法を変更するものです。具体的には、テスト実行時に作成される一時ファイルやディレクトリを、慣習的に使用されていた_testプレフィックスを持つディレクトリではなく、ioutil.TempDir関数やos.TempDir関数によって生成されるシステムの一時ディレクトリを利用するように修正しています。これにより、テスト環境のクリーンアップをより確実かつ安全に行うことを目的としています。

コミット

commit edf1c038e327f6432286aa3036d0434ea8f53907
Author: Rob Pike <r@golang.org>
Date:   Thu Feb 16 17:05:43 2012 +1100

    os: remove use of _test
    Part of issue 2573.
    
    R=dsymonds, golang-dev
    CC=golang-dev
    https://golang.org/cl/5674064

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

https://github.com/golang/go/commit/edf1c038e327f6432286aa3036d0434ea8f53907

元コミット内容

このコミットは、src/pkg/os/os_test.gosrc/pkg/os/path_test.goの2つのファイルに変更を加えています。主な変更点は、テスト内で一時ディレクトリを作成する際に、ハードコードされた_testプレフィックスを持つパスを使用する代わりに、ioutil.TempDir関数やos.TempDir関数(コミット時点ではTempDir()と記述されているが、これはosパッケージ内のヘルパー関数か、ioutil.TempDirのラッパーである可能性が高い)を利用するように変更している点です。これにより、テスト実行後の一時ディレクトリのクリーンアップがより堅牢になります。

具体的には、以下のテスト関数が影響を受けています。

  • TestStatDirWithTrailingSlash (os_test.go):
    • MkdirAll_test/_TestStatDirWithSlash_というパスを直接指定していた箇所を、ioutil.TempDir("", "/_TestStatDirWithSlash_")に置き換え。
    • defer RemoveAll(path)で一時ディレクトリの削除を遅延実行するように変更。
  • TestMkdirAll (path_test.go):
    • _test/_TestMkdirAll_/dir/./dir2というパスを直接指定していた箇所を、tmpDir := TempDir(); path := tmpDir + "/_TestMkdirAll_/dir/./dir2"のように、TempDir()で取得した一時ディレクトリのパスを基点にするように変更。
    • defer RemoveAll("_test/_TestMkdirAll_")defer RemoveAll(tmpDir + "/_TestMkdirAll_")に変更。
    • Windows環境でのパス指定も同様にtmpDirを基点にするように変更。
  • TestRemoveAll (path_test.go):
    • _test/_TestRemoveAll_というパスを直接指定していた箇所を、tmpDir := TempDir(); path := tmpDir + "/_TestRemoveAll_"のように変更。
  • TestMkdirAllWithSymlink (path_test.go):
    • _test/dir_test/linkといったパスを直接指定していた箇所を、tmpDir := TempDir()で取得した一時ディレクトリのパスを基点にするように変更。

これらの変更により、テストが実行される環境に依存せず、一時ファイルが予測可能な場所に作成され、確実にクリーンアップされるようになります。

変更の背景

このコミットは、Go言語のIssue 2573「osパッケージのテストが_testディレクトリを汚染する」の一部として行われました。

従来のGoのテストでは、一時ファイルやディレクトリを作成する際に、テストコードの近くに_testというプレフィックスを持つディレクトリを作成し、その中に一時データを配置する慣習がありました。しかし、この方法はいくつかの問題を引き起こしていました。

  1. クリーンアップの不確実性: テストが異常終了した場合や、テストランナーが適切にクリーンアップを行わない場合、_testディレクトリとその内容が残存し、ファイルシステムを汚染する可能性がありました。これは、特にCI/CD環境や開発者のローカル環境で、テストの再実行時に予期せぬ挙動を引き起こしたり、ディスクスペースを消費したりする原因となります。
  2. パスの衝突: 複数のテストが同じ_testディレクトリ内のパスを使用しようとした場合、衝突が発生し、テストの信頼性が低下する可能性がありました。
  3. 環境依存性: _testディレクトリの作成場所がテストコードの相対パスに依存するため、テストが実行されるカレントディレクトリによっては予期せぬ場所に作成される可能性がありました。

これらの問題を解決するため、Goの標準ライブラリでは、一時ファイルやディレクトリの作成に特化したio/ioutilパッケージのTempDir関数やTempFile関数(Go 1.16以降はos.MkdirTempos.CreateTempに移行)の使用が推奨されるようになりました。これらの関数は、OSが提供する一時ディレクトリの場所にユニークな名前でディレクトリやファイルを作成し、テスト終了後のクリーンアップを容易にするためのメカニズムを提供します。

このコミットは、osパッケージのテストコードを、この新しい推奨される一時ディレクトリ管理のパラダイムに移行させるための具体的なステップでした。

前提知識の解説

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

  1. Go言語のテスト:

    • Go言語では、テストファイルは通常、テスト対象のソースファイルと同じディレクトリに_test.goというサフィックスを付けて配置されます。
    • テスト関数はTestで始まり、*testing.T型の引数を取ります。
    • t.Fatalf(): テストを失敗させ、メッセージを出力してテスト関数を即座に終了します。
    • deferステートメント: deferに続く関数呼び出しは、その関数がリターンする直前に実行されます。これは、リソースのクリーンアップ(例: 一時ファイルの削除)によく使用されます。
  2. osパッケージ:

    • os.MkdirAll(path string, perm os.FileMode) error: 指定されたパスにディレクトリを作成します。途中のディレクトリが存在しない場合は、それらも作成します。permは作成されるディレクトリのパーミッションを指定します。
    • os.RemoveAll(path string) error: 指定されたパスのファイルまたはディレクトリを、その内容すべてを含めて削除します。
    • os.Stat(name string) (fi os.FileInfo, err error): 指定されたファイルまたはディレクトリの情報を返します。ファイルが存在しない場合やアクセスできない場合はエラーを返します。
    • os.Symlink(oldname, newname string) error: oldnameを指すnewnameというシンボリックリンクを作成します。
    • os.TempDir() string: システムの一時ディレクトリのデフォルトパスを返します。
  3. io/ioutilパッケージ (Go 1.16以降はosパッケージに移行):

    • ioutil.TempDir(dir, pattern string) (name string, err error): dirで指定されたディレクトリ(空文字列の場合はシステムの一時ディレクトリ)内に、patternに基づいてユニークな名前の一時ディレクトリを作成します。作成されたディレクトリの絶対パスを返します。この関数は、テストやアプリケーションで一時的な作業スペースが必要な場合に非常に便利です。
  4. ファイルパスの操作:

    • Go言語では、パスの区切り文字はOSによって異なります(Unix系では/、Windowsでは\)。path/filepathパッケージを使用することで、OSに依存しないパス操作が可能です。このコミットでは、Windowsパスのテストケースでバッククォート( )を使用して生文字列リテラルを表現し、バックスラッシュをエスケープせずに記述しています。
    • runtime.GOOS: 現在のオペレーティングシステムを示す文字列定数(例: "linux", "windows", "darwin")。これを使用して、OS固有の処理を条件分岐させることができます。
  5. Issue Tracking System (GoのIssue 2573):

    • Goプロジェクトでは、バグ報告や機能改善の提案はIssueとして管理されます。Issue番号は、関連するコミットメッセージに記載されることがよくあります。Issue 2573は、osパッケージのテストが_testディレクトリを適切にクリーンアップしない問題に関するものでした。

これらの知識を前提として、コミットの変更内容と意図を深く理解することができます。

技術的詳細

このコミットの技術的詳細な変更点は、テストにおける一時ディレクトリの生成と管理のパラダイムシフトにあります。

1. _testディレクトリからの脱却

以前のテストコードでは、一時ディレクトリを_test/というプレフィックスを持つパス(例: _test/_TestStatDirWithSlash_)で直接指定していました。これは、テストコードが実行されるディレクトリの直下に_testというディレクトリを作成し、その中にテスト用の一時データを格納するという慣習的な方法でした。

しかし、この方法には以下の問題がありました。

  • 手動クリーンアップの必要性: テストが正常終了した場合でも、defer RemoveAll("_test/...")のように明示的にRemoveAllを呼び出す必要がありました。テストがクラッシュした場合、このdeferが実行されず、一時ディレクトリが残存する可能性がありました。
  • パスの衝突と予測不能性: 複数のテストが同じ_testディレクトリ内のパスを使用しようとすると、競合状態や予期せぬ副作用が発生する可能性がありました。また、テストが実行されるカレントディレクトリによっては、_testディレクトリが開発者の意図しない場所に作成されることもありました。

2. ioutil.TempDirの導入

このコミットでは、os_test.goTestStatDirWithTrailingSlash関数において、ioutil.TempDir関数が導入されました。

// 変更前
// path := "_test/_TestStatDirWithSlash_"
// err := MkdirAll(path, 0777)

// 変更後
path, err := ioutil.TempDir("", "/_TestStatDirWithSlash_")
if err != nil {
    t.Fatalf("TempDir: %s", err)
}
defer RemoveAll(path)

ioutil.TempDir("", "/_TestStatDirWithSlash_")の呼び出しは、以下の利点をもたらします。

  • システムの一時ディレクトリの利用: 第一引数に空文字列""を渡すことで、OSが提供する標準の一時ディレクトリ(Linux/macOSでは/tmp、Windowsでは%TEMP%など)内に一時ディレクトリが作成されます。これにより、テストがファイルシステムを汚染するリスクが低減されます。
  • ユニークなディレクトリ名の生成: 第二引数の"/_TestStatDirWithSlash_"は、作成されるディレクトリ名のプレフィックスとして使用されます。ioutil.TempDirは、このプレフィックスにランダムな文字列を付加することで、ユニークなディレクトリ名を生成します。これにより、テスト間のパスの衝突が回避されます。
  • 自動的なクリーンアップの容易さ: defer RemoveAll(path)と組み合わせることで、テスト関数が終了する際に、作成された一時ディレクトリが確実に削除されるようになります。ioutil.TempDirが返すパスは、そのテスト実行に固有のものであるため、他のテストやシステムに影響を与えることなく安全に削除できます。

3. TempDir()ヘルパー関数の利用

path_test.goのテスト関数では、TempDir()というヘルパー関数が導入され、これを使用して一時ディレクトリのベースパスを取得しています。

// 変更前
// path := "_test/_TestMkdirAll_/dir/./dir2"

// 変更後
tmpDir := TempDir() // おそらく os.TempDir() またはそのラッパー
path := tmpDir + "/_/_TestMkdirAll_/dir/./dir2"

このTempDir()関数は、おそらくos.TempDir()を呼び出すか、またはioutil.TempDirと同様にユニークな一時ディレクトリを生成する内部ヘルパー関数であると考えられます。これにより、_testというハードコードされたプレフィックスを削除し、OSが管理する一時ディレクトリのパスを基点としてテスト用の一時パスを構築できるようになります。

4. Windowsパスの扱い

TestMkdirAll関数内には、runtime.GOOS == "windows"という条件分岐があり、Windows固有のパス形式(バックスラッシュ\)をテストしています。この部分も、tmpDirを基点とするように修正されています。

// 変更前
// path := `_test\_TestMkdirAll_\dir\.\dir2\`

// 変更後
path := tmpDir + `\_TestMkdirAll_\dir\.\dir2\`

Goの生文字列リテラル(バッククォートで囲まれた文字列)を使用することで、バックスラッシュをエスケープせずにそのまま記述できるため、Windowsパスの表現が簡潔になります。

まとめ

このコミットは、Goのテストにおける一時ファイル管理のベストプラクティスへの移行を示しています。ioutil.TempDiros.TempDirのような標準ライブラリの機能を利用することで、テストの信頼性、独立性、および環境への影響を大幅に改善しています。これにより、テストがより堅牢になり、開発者がテスト環境のクリーンアップについて心配する必要がなくなります。

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

このコミットにおけるコアとなるコードの変更箇所は、src/pkg/os/os_test.gosrc/pkg/os/path_test.go内の、一時ディレクトリの作成とクリーンアップに関する部分です。

src/pkg/os/os_test.go

func TestStatDirWithTrailingSlash(t *testing.T)

--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -985,25 +985,24 @@ func TestAppend(t *testing.T) {
 }
 
 func TestStatDirWithTrailingSlash(t *testing.T) {
 -	// Create new dir, in _test so it will get
 -	// cleaned up by make if not by us.
 -	path := "_test/_TestStatDirWithSlash_"
 -	err := MkdirAll(path, 0777)
 +	// Create new temporary directory and arrange to clean it up.
 +	path, err := ioutil.TempDir("", "/_TestStatDirWithSlash_")
  	if err != nil {
 -		t.Fatalf("MkdirAll %q: %s", path, err)
 +		t.Fatalf("TempDir: %s", err)
  	}
  	defer RemoveAll(path)
  
  	// Stat of path should succeed.
  	_, err = Stat(path)
  	if err != nil {
 -		t.Fatal("stat failed:", err)
 +		t.Fatalf("stat %s failed: %s", path, err)
  	}
  
  	// Stat of path+"/" should succeed too.
 -	_, err = Stat(path + "/")
 +	path += "/"
 +	_, err = Stat(path)
  	if err != nil {
 -		t.Fatal("stat failed:", err)
 +		t.Fatalf("stat %s failed: %s", path, err)
  	}
 }

src/pkg/os/path_test.go

func TestMkdirAll(t *testing.T)

--- a/src/pkg/os/path_test.go
+++ b/src/pkg/os/path_test.go
@@ -12,14 +12,13 @@ import (
 )
 
 func TestMkdirAll(t *testing.T) {
 -	// Create new dir, in _test so it will get
 -	// cleaned up by make if not by us.
 -	path := "_test/_TestMkdirAll_/dir/./dir2"
 +	tmpDir := TempDir()
 +	path := tmpDir + "_/_TestMkdirAll_/dir/./dir2"
  	err := MkdirAll(path, 0777)
  	if err != nil {
  		t.Fatalf("MkdirAll %q: %s", path, err)
  	}
 -	defer RemoveAll("_test/_TestMkdirAll_")
 +	defer RemoveAll(tmpDir + "/_TestMkdirAll_")
  
  	// Already exists, should succeed.
  	err = MkdirAll(path, 0777)
@@ -63,7 +62,7 @@ func TestMkdirAll(t *testing.T) {
  	}
  
  	if runtime.GOOS == "windows" {
 -		path := `_test\_TestMkdirAll_\dir\.\dir2\`
 +		path := tmpDir + `\_TestMkdirAll_\dir\.\dir2\`
  		err := MkdirAll(path, 0777)
  		if err != nil {
  			t.Fatalf("MkdirAll %q: %s", path, err)

func TestRemoveAll(t *testing.T)

--- a/src/pkg/os/path_test.go
+++ b/src/pkg/os/path_test.go
@@ -72,8 +71,9 @@ func TestMkdirAll(t *testing.T) {
 }
 
 func TestRemoveAll(t *testing.T) {
 +	tmpDir := TempDir()
  	// Work directory.
 -	path := "_test/_TestRemoveAll_"
 +	path := tmpDir + "/_TestRemoveAll_"
  	fpath := path + "/file"
  	dpath := path + "/dir"

func TestMkdirAllWithSymlink(t *testing.T)

--- a/src/pkg/os/path_test.go
+++ b/src/pkg/os/path_test.go
@@ -170,19 +170,22 @@ func TestMkdirAllWithSymlink(t *testing.T) {
  		return
  	}
  
 -	err := Mkdir("_test/dir", 0755)
 +	tmpDir := TempDir()
 +	dir := tmpDir + "/dir"
 +	err := Mkdir(dir, 0755)
  	if err != nil {
 -		t.Fatal(`Mkdir "_test/dir":`, err)
 +		t.Fatalf("Mkdir %s: %s", dir, err)
  	}
 -	defer RemoveAll("_test/dir")
 +	defer RemoveAll(dir)
  
 -	err = Symlink("dir", "_test/link")
 +	link := tmpDir + "/link"
 +	err = Symlink("dir", link)
  	if err != nil {
 -		t.Fatal(`Symlink "dir", "_test/link":`, err)
 +		t.Fatalf("Symlink %s: %s", link, err)
  	}
 -	defer RemoveAll("_test/link")
 +	defer RemoveAll(link)
  
 -	path := "_test/link/foo"
 +	path := link + "/foo"
  	err = MkdirAll(path, 0755)
  	if err != nil {
  		t.Errorf("MkdirAll %q: %s", path, err)

コアとなるコードの解説

上記の変更箇所は、Go言語のテストにおける一時ファイル/ディレクトリの管理方法を、より堅牢で標準的なアプローチに移行させるためのものです。

  1. ioutil.TempDirの利用 (os_test.go):

    • 変更前は、_testという固定のプレフィックスを持つディレクトリをMkdirAllで作成していました。これは、テストが実行されるディレクトリに依存し、クリーンアップが不確実になる可能性がありました。
    • 変更後は、ioutil.TempDir("", "/_TestStatDirWithSlash_")を使用しています。
      • 第一引数の""は、OSが提供する標準の一時ディレクトリ(例: /tmp)を使用することを意味します。
      • 第二引数の"/_TestStatDirWithSlash_"は、作成される一時ディレクトリ名のプレフィックスとなります。ioutil.TempDirはこれにランダムな文字列を付加し、ユニークなディレクトリ名を生成します。
    • これにより、テストはシステムの一時領域に独立した作業ディレクトリを持ち、他のテストやシステムへの影響を最小限に抑えられます。
    • defer RemoveAll(path)は、テスト関数が終了する際に、作成された一時ディレクトリとその内容を確実に削除するためのGoのイディオムです。ioutil.TempDirが返すユニークなパスに対してRemoveAllを適用することで、クリーンアップの信頼性が向上します。
    • エラーメッセージもMkdirAllからTempDirに、そしてstat failedからstat %s failedに、より具体的になるように修正されています。
  2. TempDir()ヘルパー関数の導入と利用 (path_test.go):

    • path_test.goの複数のテスト関数(TestMkdirAll, TestRemoveAll, TestMkdirAllWithSymlink)では、tmpDir := TempDir()という行が追加されています。
    • このTempDir()関数は、おそらくos.TempDir()を呼び出すか、またはioutil.TempDirと同様にユニークな一時ディレクトリを生成する内部ヘルパー関数であると考えられます。これにより、テスト用の一時パスを構築する際の基点として、ハードコードされた_testプレフィックスではなく、動的に取得される一時ディレクトリのパスを使用するようになります。
    • 例えば、path := tmpDir + "_/_TestMkdirAll_/dir/./dir2"のように、tmpDirを基点として相対パスを結合することで、テストが実行される環境に依存しない一時パスが生成されます。
    • クリーンアップのdefer RemoveAllも、_test/...という固定パスからtmpDir + "/..."という動的なパスに変更され、作成された一時ディレクトリが確実に削除されるように修正されています。
    • Windows固有のパスをテストする箇所でも、同様にtmpDirを基点とするように変更されており、OS間のパス表現の違いを吸収しつつ、一時ディレクトリの管理を統一しています。

これらの変更は、Goのテストの堅牢性と信頼性を高める上で非常に重要です。一時ディレクトリの適切な管理は、テストの再現性を保証し、開発環境やCI/CDパイプラインの安定性を維持するために不可欠です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (pkg.go.dev)
  • Go言語のGitHubリポジトリ (特にIssueトラッカーとコミット履歴)
  • Go言語のコードレビューシステム (golang.org/cl)
  • Go言語のテストに関する一般的なプラクティスとガイドライン
  • ファイルシステム操作に関する一般的な知識
  • Go言語のdeferステートメントの動作に関する知識
  • Go言語のruntimeパッケージに関する知識
  • Go言語のパス操作に関する知識
  • Go言語のIssue 2573に関する議論内容 (GitHub Issueページ)
  • io/ioutilパッケージからosパッケージへの一時ファイル/ディレクトリ関数の移行に関する情報 (Go 1.16のリリースノートなど)