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

[インデックス 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.」という一文は、シンボリックリンクの取り扱いがファイルシステム操作において複雑で、しばしば予期せぬ問題を引き起こすことへの開発者のフラストレーションを示唆しています。この変更は、シンボリックリンクのテストをより堅牢にし、環境に依存しない形で正確に動作させることを目的としています。

前提知識の解説

シンボリックリンクは、ファイルシステム上の別のファイルやディレクトリへの参照(ポインタ)として機能する特殊なファイルです。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 のテスト方法を根本的に見直したことです。

  1. 一時ディレクトリの利用: 以前のテストは、os.RemoveAll("test") のように、カレントディレクトリに test という名前のディレクトリを作成し、それをクリーンアップしていました。これは、テストが実行される環境に依存し、他のファイルとの衝突や、テスト実行後のクリーンアップ漏れのリスクがありました。 新しいテストでは、ioutil.TempDir("", "evalsymlink") を使用して、システムの一時ディレクトリ内にユニークな名前の一時ディレクトリを作成します。これにより、テストが完全に分離され、クリーンアップも defer os.RemoveAll(tmpDir) によって確実に行われるようになります。

  2. 一時ディレクトリのシンボリックリンク解決: tmpDir, err = filepath.EvalSymlinks(tmpDir) という行が追加されています。これは、ioutil.TempDir が作成した一時ディレクトリ自体が、システムによってはシンボリックリンクである可能性があるためです(例: /tmp/private/tmp へのシンボリックリンクであるmacOSなど)。テストの信頼性を高めるため、テスト対象の EvalSymlinks 関数を使って、この一時ディレクトリの実際の物理パスを取得しています。これにより、テストがシンボリックリンクの連鎖の途中で開始されることを防ぎ、テストの基盤をより堅牢にしています。

  3. 相対パスでのシンボリックリンクファーム構築: テストで使用されるシンボリックリンクの構造("symlink farm")は、simpleJoin(tmpDir, d.path) を使用して、新しく作成された一時ディレクトリを基準に構築されます。これにより、テストが絶対パスに依存せず、どの環境でも同じ相対的なファイルシステム構造を再現できるようになります。

  4. テストの簡素化と絶対パス依存の排除: 以前のテストコードには、GOROOT 環境変数を使用して絶対パスを構築し、相対パスと絶対パスの両方で EvalSymlinks をテストするロジックが含まれていました。このアプローチは、GOROOT がシンボリックリンクである場合に問題を引き起こす可能性がありました。 新しいテストでは、一時ディレクトリ内に相対的なシンボリックリンク構造を作成し、その構造内で EvalSymlinks をテストします。これにより、テストが GOROOT やその他の環境変数に依存することなく、EvalSymlinks のコアな機能を独立して検証できるようになります。Windows固有の絶対パスのテストケースは残されていますが、これはWindowsのパスの特性に対応するためです。

  5. 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 と異なり、.. などのパス要素を評価しないため、テストで特定のシンボリックリンク構造を正確に作成する際に役立ちます。

  • テストの無効化コメントの削除: 以前のコミットでテストが無効化されていたコメント 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言語の公式ドキュメント: path/filepath パッケージ
  • Go言語の公式ドキュメント: os パッケージ
  • Go言語の公式ドキュメント: io/ioutil パッケージ (Go 1.16以降は os および io パッケージに統合)
  • Go言語の公式ドキュメント: runtime パッケージ
  • シンボリックリンクに関する一般的な情報源 (Wikipediaなど)
  • Go言語のテストに関する一般的な情報源