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

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

このコミットは、Go言語の標準ライブラリosパッケージのテストコードos_test.goにおける変更です。具体的には、TestStartProcess関数内でプロセスの実行結果を検証する際に、ファイルの同一性を比較するためにos.SameFile関数を使用するように修正されています。これにより、異なるパス表記でも同じファイルを参照している場合にテストが正しくパスするようになります。

コミット

commit 40b3758864deab9e096b6ee8d102caf11ecce051
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Thu Jan 17 18:48:11 2013 +0800

    os: use SameFile in TestStartProcess
    Fixes #4650.
    
    R=golang-dev, bradfitz, alex.brainman
    CC=golang-dev
    https://golang.org/cl/7085048

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

https://github.com/golang/go/commit/40b3758864deab9e096b6ee8d102caf11ecce051

元コミット内容

このコミットの元のメッセージは以下の通りです。

os: use SameFile in TestStartProcess
Fixes #4650.

R=golang-dev, bradfitz, alex.brainman
CC=golang-dev
https://golang.org/cl/7085048

これは、osパッケージのTestStartProcessにおいてSameFileを使用すること、そしてそれがIssue #4650を修正することを示しています。

変更の背景

この変更の背景には、TestStartProcessテストが特定の環境(特にSolarisのような/bin/usr/binへのシンボリックリンクであるシステム)で失敗するという問題がありました。

元のコードでは、execヘルパー関数内でプロセスの出力(通常は現在の作業ディレクトリのパス)と期待されるパスを文字列として直接比較していました。しかし、ファイルシステムによっては、同じディレクトリを指すパスが異なる文字列として表現されることがあります(例: /bin/usr/binが同じ実体を指す場合)。

具体的には、Solarisでは/bin/usr/binへのシンボリックリンクであるため、pwdコマンドが/usr/binを返すことがありましたが、テストコードでは/binを期待していました。この文字列の不一致がテストの失敗を引き起こしていました。

この問題を解決するため、パスの文字列比較ではなく、それらのパスが指すファイルシステム上の実体が同一であるかどうかを比較するos.SameFile関数が導入されました。これにより、パスの表記方法に依存せず、実体としての同一性を確認できるようになり、テストの堅牢性が向上しました。

前提知識の解説

os.SameFile関数

os.SameFile(fi1, fi2 os.FileInfo) boolは、Go言語のosパッケージで提供される関数です。この関数は、2つのos.FileInfoインターフェース(ファイルやディレクトリのメタデータを含む構造体)を受け取り、それらが同じファイルシステム上の同じファイルを指している場合にtrueを返します。

SameFileは、ファイル名やパスの文字列が異なっていても、それがシンボリックリンクやハードリンク、あるいはファイルシステム固有の特性によって同じ実体を指している場合に真を返します。これは、ファイルシステム上のinode番号やデバイスIDなどの低レベルな情報に基づいて比較を行うことで実現されます。

os.FileInfoインターフェース

os.FileInfoは、ファイルに関する情報(ファイル名、サイズ、パーミッション、最終更新時刻、ディレクトリかどうかなど)を提供するインターフェースです。os.Statos.Lstatなどの関数を呼び出すことで、特定のパスに対応するos.FileInfoオブジェクトを取得できます。

シンボリックリンクとハードリンク

  • シンボリックリンク (Symbolic Link / Soft Link): 別のファイルやディレクトリへの参照(ポインタ)です。シンボリックリンク自体は小さなファイルで、リンク先のパスを格納しています。リンク先が削除されると、シンボリックリンクは「壊れたリンク」になります。
  • ハードリンク (Hard Link): 同じファイルシステム上の既存のファイルへの追加のエントリです。ハードリンクは、元のファイルと同じinode番号を共有します。つまり、同じファイルシステム上の同じデータブロックを指す複数の名前が存在する状態です。いずれかのリンクが削除されても、他のリンクが存在する限りファイルデータは残ります。

このコミットの背景にある問題は、シンボリックリンクによって同じファイルが異なるパスで参照されるケースに関連しています。

技術的詳細

変更はsrc/pkg/os/os_test.goファイル内のexecヘルパー関数とTestStartProcess関数に集中しています。

execヘルパー関数の変更

元のexec関数では、プロセスの出力outputと期待される値expectを直接文字列比較していました。Solarisの/usrプレフィックスを許容するための特別な条件も含まれていました。

// Before
if output != expect && output != "/usr"+expect {
    t.Errorf("exec %q returned %q wanted %q",
        strings.Join(append([]string{cmd}, args...), " "), output, expect)
}

この部分が以下のように変更されました。

  1. outputexpectのそれぞれについて、os.Statを呼び出してos.FileInfoオブジェクトを取得します。strings.TrimSpace(output)は、出力に含まれる可能性のある改行コードなどを除去するためです。
  2. 取得した2つのos.FileInfoオブジェクトをos.SameFile関数に渡して比較します。
  3. SameFilefalseを返した場合(つまり、ファイルシステム上の実体が異なる場合)にテストエラーとします。
// After
fi1, _ := Stat(strings.TrimSpace(output))
fi2, _ := Stat(expect)
if !SameFile(fi1, fi2) {
    t.Errorf("exec %q returned %q wanted %q",
        strings.Join(append([]string{cmd}, args...), " "), output, expect)
}

この変更により、パスの文字列表現の違いを吸収し、ファイルシステム上の実体としての同一性を正確に検証できるようになりました。

TestStartProcess関数の変更

TestStartProcess関数では、WindowsとUnix系システムで異なる改行コード(\r\n\n)を考慮するためにle(line ending)変数を使用していました。

// Before
var dir, cmd, le string
// ...
if runtime.GOOS == "windows" {
    le = "\r\n"
    // ...
} else {
    le = "\n"
    // ...
}
// ...
exec(t, dir, cmd, args, dir+le)
exec(t, cmddir, cmdbase, args, filepath.Clean(cmddir)+le)

SameFileによる比較では、ファイルパスの文字列自体に改行コードが含まれている必要がないため、le変数は不要になりました。

// After
var dir, cmd string // 'le' variable removed
// ...
// No 'le' assignment
// ...
exec(t, dir, cmd, args, dir) // 'le' removed from arguments
exec(t, cmddir, cmdbase, args, cmddir) // 'le' removed from arguments

これにより、テストコードが簡潔になり、プラットフォームごとの改行コードの違いを意識する必要がなくなりました。

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

変更はsrc/pkg/os/os_test.goファイルにあります。

--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -536,8 +536,10 @@ func exec(t *testing.T, dir, cmd string, args []string, expect string) {
 	var b bytes.Buffer
 	io.Copy(&b, r)
 	output := b.String()
-	// Accept /usr prefix because Solaris /bin is symlinked to /usr/bin.
-	if output != expect && output != "/usr"+expect {
+
+	fi1, _ := Stat(strings.TrimSpace(output))
+	fi2, _ := Stat(expect)
+	if !SameFile(fi1, fi2) {
 		t.Errorf("exec %q returned %q wanted %q",
 			strings.Join(append([]string{cmd}, args...), " "), output, expect)
 	}
@@ -545,15 +547,13 @@ func exec(t *testing.T, dir, cmd string, args []string, expect string) {
 }
 
 func TestStartProcess(t *testing.T) {
-	var dir, cmd, le string
+	var dir, cmd string
 	var args []string
 	if runtime.GOOS == "windows" {
-\t\tle = "\r\n"
 		cmd = Getenv("COMSPEC")
 		dir = Getenv("SystemRoot")
 		args = []string{"/c", "cd"}
 	} else {
-\t\tle = "\n"
 		cmd = "/bin/pwd"
 		dir = "/"
 		args = []string{}
@@ -561,9 +561,9 @@ func TestStartProcess(t *testing.T) {
 	cmddir, cmdbase := filepath.Split(cmd)
 	args = append([]string{cmdbase}, args...)
 	// Test absolute executable path.
-\texec(t, dir, cmd, args, dir+le)
+\texec(t, dir, cmd, args, dir)
 	// Test relative executable path.
-\texec(t, cmddir, cmdbase, args, filepath.Clean(cmddir)+le)
+\texec(t, cmddir, cmdbase, args, cmddir)
 }
 
 func checkMode(t *testing.T, path string, mode FileMode) {

コアとなるコードの解説

exec関数内の変更

  • 変更前:

    if output != expect && output != "/usr"+expect {
        // ...
    }
    

    ここでは、output文字列がexpect文字列と完全に一致するか、またはSolaris環境での特殊なケースとして/usrプレフィックスが付いたexpectと一致するかをチェックしていました。これは文字列ベースの比較であり、ファイルシステム上の同一性を保証するものではありませんでした。

  • 変更後:

    fi1, _ := Stat(strings.TrimSpace(output))
    fi2, _ := Stat(expect)
    if !SameFile(fi1, fi2) {
        // ...
    }
    

    この変更がこのコミットの核心です。

    1. strings.TrimSpace(output): プロセスからの出力(output)には末尾に改行コードが含まれる可能性があるため、strings.TrimSpaceで空白文字(改行含む)を除去し、純粋なパス文字列を取得します。
    2. Stat(...): os.Stat関数は指定されたパスのos.FileInfoを返します。これはファイルシステム上のファイルやディレクトリに関するメタデータを含みます。エラーハンドリングはテストコードなので省略されていますが、実際にはエラーチェックが必要です。
    3. SameFile(fi1, fi2): 取得した2つのos.FileInfoオブジェクト(fi1fi2)をos.SameFile関数に渡します。この関数は、これら2つのFileInfoがファイルシステム上の同じ実体を指している場合にtrueを返します。
    4. !SameFile(fi1, fi2): SameFilefalseを返した場合、つまり実体が異なる場合にテストエラーを発生させます。

この変更により、パスの文字列表現の違い(例: /bin/usr/bin)を吸収し、ファイルシステム上の実体としての同一性を正確に検証できるようになりました。これにより、シンボリックリンクなどが絡む環境でもテストが安定して動作するようになります。

TestStartProcess関数内の変更

  • 変更前:

    var dir, cmd, le string
    // ...
    if runtime.GOOS == "windows" {
        le = "\r\n"
    } else {
        le = "\n"
    }
    // ...
    exec(t, dir, cmd, args, dir+le)
    exec(t, cmddir, cmdbase, args, filepath.Clean(cmddir)+le)
    

    WindowsとUnix系システムで異なる改行コード(\r\n\n)を考慮するためにle変数を使用していました。exec関数に渡す期待値のパスにこの改行コードを付加していました。

  • 変更後:

    var dir, cmd string // 'le'変数が削除された
    // ...
    // 'le'の割り当てがなくなった
    // ...
    exec(t, dir, cmd, args, dir) // 'le'が引数から削除された
    exec(t, cmddir, cmdbase, args, cmddir) // 'le'が引数から削除された
    

    exec関数がSameFileを使用してファイルシステム上の実体を比較するようになったため、期待されるパス文字列に改行コードを含める必要がなくなりました。これにより、le変数が不要になり、コードが簡潔になりました。

このコミットは、Goのテストコードの堅牢性を高め、異なるOS環境での互換性を向上させるための重要な改善です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GitHubのGoリポジトリのIssueとChange List (CL)
  • Stack Overflowや技術ブログなど、Go言語のos.SameFileに関する一般的な解説