[インデックス 11665] ファイルの概要
このコミットは、Go言語の標準ライブラリ path/filepath
パッケージにおけるシンボリックリンク(symlink)のテストコードの修正と簡素化を目的としています。特に、filepath.EvalSymlinks
関数のテストが対象となっており、既存のテストの信頼性と堅牢性を向上させる変更が加えられています。
コミット
commit 97ef43721274dab8a88b60e362d37eea077b66be
Author: Rob Pike <r@golang.org>
Date: Tue Feb 7 11:00:13 2012 +1100
path/filepath: repair and simplify the symlink test
I hate symlinks.
Fixes #2787.
R=golang-dev, dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5638043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/97ef43721274dab8a88b60e362d37eea077b66be
元コミット内容
path/filepath: repair and simplify the symlink test
I hate symlinks.
Fixes #2787.
変更の背景
このコミットの背景には、path/filepath
パッケージの EvalSymlinks
関数に関連する既存のテストの不具合、または不十分さがありました。コミットメッセージにある Fixes #2787
から、特定のバグ報告(Issue 2787)に対応するものであることがわかります。
Issue 2787は、filepath.EvalSymlinks
のテストが、GOROOT
環境変数に依存しているために、テストの実行環境によって結果が不安定になる問題について言及しています。特に、GOROOT
がシンボリックリンクである場合にテストが失敗するという報告がありました。これは、テストが絶対パスを生成する際に GOROOT
を基準にしているため、シンボリックリンクの解決が正しく行われないと、期待されるパスと実際のパスが一致しないという問題を引き起こしていました。
コミットメッセージの「I hate symlinks.」という一文は、シンボリックリンクの取り扱いがファイルシステム操作において複雑で、しばしば予期せぬ問題を引き起こすことへの開発者のフラストレーションを示唆しています。この変更は、シンボリックリンクのテストをより堅牢にし、環境に依存しない形で正確に動作させることを目的としています。
前提知識の解説
1. シンボリックリンク (Symbolic Link / Symlink)
シンボリックリンクは、ファイルシステム上の別のファイルやディレクトリへの参照(ポインタ)として機能する特殊なファイルです。Windowsでは「ショートカット」に似ていますが、OSレベルで透過的に扱われる点が異なります。シンボリックリンクをたどると、参照先のファイルやディレクトリにアクセスできます。
- ハードリンクとの違い: ハードリンクは同じiノードを指すため、元のファイルが削除されてもデータは残りますが、シンボリックリンクは参照先のパスを保持するため、参照先が削除されるとリンクは「壊れた」状態になります。
- 用途: ソフトウェアのバージョン管理、設定ファイルの共有、ディスク容量の節約、パスの抽象化などに利用されます。
2. path/filepath
パッケージ
Go言語の標準ライブラリの一部で、ファイルパスの操作に関する機能を提供します。OS固有のパス区切り文字(Unix系では/
、Windowsでは\
)を抽象化し、クロスプラットフォームで動作するパス操作を可能にします。
filepath.EvalSymlinks(path string) (string, error)
: この関数は、与えられたパスに含まれるシンボリックリンクを再帰的に解決し、最終的な物理パスを返します。例えば、/home/user/link_to_dir
が/var/data
へのシンボリックリンクである場合、EvalSymlinks("/home/user/link_to_dir/file.txt")
は/var/data/file.txt
を返します。
3. os.TempDir()
と ioutil.TempDir()
一時ファイルや一時ディレクトリを作成するための関数です。
os.TempDir()
: システムの一時ディレクトリのパスを返します(例:/tmp
on Linux,C:\Users\...\AppData\Local\Temp
on Windows)。ioutil.TempDir(dir, pattern string) (name string, err error)
: 指定されたディレクトリ(dir
が空の場合はシステムの一時ディレクトリ)内に、指定されたパターン(pattern
)に基づいてユニークな名前の一時ディレクトリを作成します。この関数は、テストなどで一時的なファイルシステム構造を安全に作成し、テスト終了後にクリーンアップするのに非常に便利です。
4. defer os.RemoveAll(path)
Go言語の defer
ステートメントは、関数がリターンする直前に指定された関数を実行します。os.RemoveAll(path)
は、指定されたパスにあるファイルまたはディレクトリとその内容をすべて削除します。
この組み合わせは、一時的なリソース(この場合は一時ディレクトリ)をテストの開始時に作成し、テストの終了時に確実にクリーンアップするために頻繁に使用されるイディオムです。これにより、テストが失敗した場合でも、一時ファイルがシステムに残ることを防ぎます。
5. runtime.GOOS
Go言語の組み込み変数で、プログラムが実行されているオペレーティングシステムの名前(例: "linux", "windows", "darwin")を文字列で返します。これにより、OS固有の処理を条件分岐させることができます。シンボリックリンクの作成(os.Symlink
)はWindowsでは挙動が異なる、またはサポートされていない場合があるため、この変数で条件分岐を行うのは一般的なプラクティスです。
技術的詳細
このコミットの主要な技術的変更点は、filepath.EvalSymlinks
のテスト方法を根本的に見直したことです。
-
一時ディレクトリの利用: 以前のテストは、
os.RemoveAll("test")
のように、カレントディレクトリにtest
という名前のディレクトリを作成し、それをクリーンアップしていました。これは、テストが実行される環境に依存し、他のファイルとの衝突や、テスト実行後のクリーンアップ漏れのリスクがありました。 新しいテストでは、ioutil.TempDir("", "evalsymlink")
を使用して、システムの一時ディレクトリ内にユニークな名前の一時ディレクトリを作成します。これにより、テストが完全に分離され、クリーンアップもdefer os.RemoveAll(tmpDir)
によって確実に行われるようになります。 -
一時ディレクトリのシンボリックリンク解決:
tmpDir, err = filepath.EvalSymlinks(tmpDir)
という行が追加されています。これは、ioutil.TempDir
が作成した一時ディレクトリ自体が、システムによってはシンボリックリンクである可能性があるためです(例:/tmp
が/private/tmp
へのシンボリックリンクであるmacOSなど)。テストの信頼性を高めるため、テスト対象のEvalSymlinks
関数を使って、この一時ディレクトリの実際の物理パスを取得しています。これにより、テストがシンボリックリンクの連鎖の途中で開始されることを防ぎ、テストの基盤をより堅牢にしています。 -
相対パスでのシンボリックリンクファーム構築: テストで使用されるシンボリックリンクの構造("symlink farm")は、
simpleJoin(tmpDir, d.path)
を使用して、新しく作成された一時ディレクトリを基準に構築されます。これにより、テストが絶対パスに依存せず、どの環境でも同じ相対的なファイルシステム構造を再現できるようになります。 -
テストの簡素化と絶対パス依存の排除: 以前のテストコードには、
GOROOT
環境変数を使用して絶対パスを構築し、相対パスと絶対パスの両方でEvalSymlinks
をテストするロジックが含まれていました。このアプローチは、GOROOT
がシンボリックリンクである場合に問題を引き起こす可能性がありました。 新しいテストでは、一時ディレクトリ内に相対的なシンボリックリンク構造を作成し、その構造内でEvalSymlinks
をテストします。これにより、テストがGOROOT
やその他の環境変数に依存することなく、EvalSymlinks
のコアな機能を独立して検証できるようになります。Windows固有の絶対パスのテストケースは残されていますが、これはWindowsのパスの特性に対応するためです。 -
simpleJoin
ヘルパー関数の導入:filepath.Join
はパスを結合する際に..
などの要素を評価して正規化しますが、テストでは意図的に..
を評価させたくない場合があります。simpleJoin
関数は、単にディレクトリとパスをOSのパス区切り文字で結合するだけで、パスの評価を行いません。これにより、テストで特定のシンボリックリンクの構造を正確に作成できるようになります。
コアとなるコードの変更箇所
変更は src/pkg/path/filepath/path_test.go
ファイルに集中しています。
--- a/src/pkg/path/filepath/path_test.go
+++ b/src/pkg/path/filepath/path_test.go
@@ -5,6 +5,7 @@
package filepath_test
import (
+ "io/ioutil"
"os"
"path/filepath"
"reflect"
@@ -548,6 +549,7 @@ func TestIsAbs(t *testing.T) {
}
type EvalSymlinksTest struct {
+ // If dest is empty, the path is created; otherwise the dest is symlinked to the path.
path, dest string
}
@@ -575,34 +577,42 @@ var EvalSymlinksAbsWindowsTests = []EvalSymlinksTest{
{`c:\\`, `c:\\`},\n
}\n
-func testEvalSymlinks(t *testing.T, tests []EvalSymlinksTest) {
-\tfor _, d := range tests {\n
-\t\tif p, err := filepath.EvalSymlinks(d.path); err != nil {\n
-\t\t\tt.Errorf(\"EvalSymlinks(%q) error: %v\", d.path, err)\n
-\t\t} else if filepath.Clean(p) != filepath.Clean(d.dest) {\n
-\t\t\tt.Errorf(\"EvalSymlinks(%q)=%q, want %q\", d.path, p, d.dest)\n
-\t\t}\n
-\t}\n
+// simpleJoin builds a file name from the directory and path.\n
+// It does not use Join because we don\'t want ".." to be evaluated.\n
+func simpleJoin(dir, path string) string {\n
+\treturn dir + string(filepath.Separator) + path\n
}\n \n func TestEvalSymlinks(t *testing.T) {\n-\tt.Logf(\"test needs to be rewritten; disabled\")\n-\treturn\n+\ttmpDir, err := ioutil.TempDir(\"\", \"evalsymlink\")\n+\tif err != nil {\n+\t\tt.Fatal(\"creating temp dir:\", err)\n+\t}\n+\tdefer os.RemoveAll(tmpDir)\n \n-\tdefer os.RemoveAll(\"test\")\n+\t// /tmp may itself be a symlink! Avoid the confusion, although\n+\t// it means trusting the thing we\'re testing.\n+\ttmpDir, err = filepath.EvalSymlinks(tmpDir)\n+\tif err != nil {\n+\t\tt.Fatal(\"eval symlink for tmp dir:\", err)\n+\t}\n+\n+\t// Create the symlink farm using relative paths.\n \tfor _, d := range EvalSymlinksTestDirs {\n \t\tvar err error\n+\t\tpath := simpleJoin(tmpDir, d.path)\n \t\tif d.dest == \"\" {\n-\t\t\terr = os.Mkdir(d.path, 0755)\n+\t\t\terr = os.Mkdir(path, 0755)\n \t\t} else {\n \t\t\tif runtime.GOOS != \"windows\" {\n-\t\t\t\terr = os.Symlink(d.dest, d.path)\n+\t\t\t\terr = os.Symlink(d.dest, path)\n \t\t\t}\n \t\t}\n \t\tif err != nil {\n \t\t\tt.Fatal(err)\n \t\t}\n \t}\n+\n \tvar tests []EvalSymlinksTest\n \tif runtime.GOOS == \"windows\" {\n \t\tfor _, d := range EvalSymlinksTests {\n@@ -614,24 +624,17 @@ func TestEvalSymlinks(t *testing.T) {\n \t} else {\n \t\ttests = EvalSymlinksTests\n \t}\n-\t// relative\n-\ttestEvalSymlinks(t, tests)\n-\t// absolute\n-\tgoroot, err := filepath.EvalSymlinks(os.Getenv(\"GOROOT\"))\n-\tif err != nil {\n-\t\tt.Fatalf(\"EvalSymlinks(%q) error: %v\", os.Getenv(\"GOROOT\"), err)\n-\t}\n-\ttestroot := filepath.Join(goroot, \"src\", \"pkg\", \"path\", \"filepath\")\n-\tfor i, d := range tests {\n-\t\ttests[i].path = filepath.Join(testroot, d.path)\n-\t\ttests[i].dest = filepath.Join(testroot, d.dest)\n-\t}\n-\tif runtime.GOOS == \"windows\" {\n-\t\tfor _, d := range EvalSymlinksAbsWindowsTests {\n-\t\t\ttests = append(tests, d)\n+\n+\t// Evaluate the symlink farm.\n+\tfor _, d := range tests {\n+\t\tpath := simpleJoin(tmpDir, d.path)\n+\t\tdest := simpleJoin(tmpDir, d.dest)\n+\t\tif p, err := filepath.EvalSymlinks(path); err != nil {\n+\t\t\tt.Errorf(\"EvalSymlinks(%q) error: %v\", d.path, err)\n+\t\t} else if filepath.Clean(p) != filepath.Clean(dest) {\n+\t\t\tt.Errorf(\"Clean(%q)=%q, want %q\", path, p, dest)\n \t\t}\n \t}\n-\ttestEvalSymlinks(t, tests)\n }\n \n // Test paths relative to $GOROOT/src\n```
## コアとなるコードの解説
### 1. `import "io/ioutil"` の追加
一時ディレクトリを作成するために `ioutil.TempDir` 関数を使用するため、`io/ioutil` パッケージがインポートされています。
### 2. `EvalSymlinksTest` 構造体へのコメント追加
`EvalSymlinksTest` 構造体の `dest` フィールドに関するコメントが追加され、その役割が明確化されています。
### 3. `simpleJoin` ヘルパー関数の導入
```go
// simpleJoin builds a file name from the directory and path.
// It does not use Join because we don't want ".." to be evaluated.
func simpleJoin(dir, path string) string {
return dir + string(filepath.Separator) + path
}
この関数は、ディレクトリとパスをOSのパス区切り文字で単純に結合します。filepath.Join
と異なり、..
などのパス要素を評価しないため、テストで特定のシンボリックリンク構造を正確に作成する際に役立ちます。
4. TestEvalSymlinks
関数の大幅な変更
- テストの無効化コメントの削除: 以前のコミットでテストが無効化されていたコメント
t.Logf("test needs to be rewritten; disabled")
が削除され、テストが有効化されています。 - 一時ディレクトリの作成とクリーンアップ:
tmpDir, err := ioutil.TempDir("", "evalsymlink") if err != nil { t.Fatal("creating temp dir:", err) } defer os.RemoveAll(tmpDir)
ioutil.TempDir
を使用して一時ディレクトリを作成し、defer os.RemoveAll(tmpDir)
でテスト終了時に確実にクリーンアップするように変更されました。これにより、テストの独立性と信頼性が向上します。 - 一時ディレクトリのシンボリックリンク解決:
作成された一時ディレクトリ自体がシンボリックリンクである可能性を考慮し、tmpDir, err = filepath.EvalSymlinks(tmpDir) if err != nil { t.Fatal("eval symlink for tmp dir:", err) }
filepath.EvalSymlinks
を使ってその物理パスを取得しています。これは、テストの基盤をより堅牢にするための重要なステップです。 - シンボリックリンクファームの構築:
// Create the symlink farm using relative paths. for _, d := range EvalSymlinksTestDirs { var err error path := simpleJoin(tmpDir, d.path) // simpleJoin を使用 if d.dest == "" { err = os.Mkdir(path, 0755) // tmpDir を基準にディレクトリを作成 } else { if runtime.GOOS != "windows" { err = os.Symlink(d.dest, path) // tmpDir を基準にシンボリックリンクを作成 } } if err != nil { t.Fatal(err) } }
EvalSymlinksTestDirs
に基づいてシンボリックリンクの構造を構築する際、simpleJoin
を使用して一時ディレクトリを基準としたパスでファイルやシンボリックリンクを作成しています。これにより、テストが環境に依存しない相対的な構造で実行されます。 - テストロジックの簡素化:
以前の
testEvalSymlinks
関数は削除され、TestEvalSymlinks
関数内で直接テストロジックが記述されています。GOROOT
に依存した絶対パスのテストロジックが削除され、一時ディレクトリ内で構築された相対的なシンボリックリンクファームに対してEvalSymlinks
を実行し、結果を検証する形に変更されました。
テストケースのパスと期待される結果も、一時ディレクトリを基準とした絶対パスに変換されて比較されます。// Evaluate the symlink farm. for _, d := range tests { path := simpleJoin(tmpDir, d.path) dest := simpleJoin(tmpDir, d.dest) if p, err := filepath.EvalSymlinks(path); err != nil { t.Errorf("EvalSymlinks(%q) error: %v", d.path, err) } else if filepath.Clean(p) != filepath.Clean(dest) { t.Errorf("Clean(%q)=%q, want %q", path, p, dest) } }
filepath.Clean
を使用してパスを正規化し、比較の堅牢性を高めています。
これらの変更により、filepath.EvalSymlinks
のテストは、より独立性が高く、環境に依存せず、信頼性の高いものになりました。
関連リンク
- Go Issue 2787: https://github.com/golang/go/issues/2787
参考にした情報源リンク
- Go言語の公式ドキュメント:
path/filepath
パッケージ - Go言語の公式ドキュメント:
os
パッケージ - Go言語の公式ドキュメント:
io/ioutil
パッケージ (Go 1.16以降はos
およびio
パッケージに統合) - Go言語の公式ドキュメント:
runtime
パッケージ - シンボリックリンクに関する一般的な情報源 (Wikipediaなど)
- Go言語のテストに関する一般的な情報源