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

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

このコミットは、Go言語の標準ライブラリ path/filepath パッケージ内のテスト TestBug3486 における競合状態(race condition)を修正するものです。具体的には、filepath.Walk 関数を用いたファイルツリーの走査テストが、他のテストによってファイルが作成・削除されることで不安定になる問題を解決しています。

コミット

commit 5dc8c4dbfb6a04d9eb7a11c9c3fe698d33d0c0ee
Author: Russ Cox <rsc@golang.org>
Date:   Mon Sep 9 16:42:18 2013 -0400

    path/filepath: fix race with other tests
    
    Bug3486 tried to walk the entire file tree, but other tests might
    be creating and removing files in that tree. In particular, package os
    creates and removes files in the os directory, and issue 5863
    reports failures due to seeing those files appear and then disappear.
    
    Change the test to walk just the test tree, which should not be
    changing.
    
    Fixes #5863.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/13467045

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

https://github.com/golang/go/commit/5dc8c4dbfb6a04d9eb7a11c9c3fe698d33d0c0ee

元コミット内容

path/filepath: fix race with other tests

Bug3486 はファイルツリー全体を走査しようとしましたが、他のテストがそのツリー内でファイルを生成したり削除したりする可能性がありました。特に、os パッケージは os ディレクトリ内でファイルを生成・削除し、Issue 5863 はそれらのファイルが出現したり消えたりすることによるテストの失敗を報告しています。

テストを、変更されるべきではないテストツリーのみを走査するように変更します。

Issue #5863 を修正します。

変更の背景

このコミットの背景には、Go言語のテストスイートにおける不安定性がありました。path/filepath パッケージの TestBug3486 というテストは、filepath.Walk 関数を使用してファイルシステムを走査し、特定のディレクトリ(libsrc)が適切に処理されることを検証していました。

しかし、このテストが runtime.GOROOT() で示されるGoのインストールディレクトリ全体を走査しようとした際、同じテストスイート内で並行して実行されている他のテスト(特に os パッケージのテストなど)が、その走査対象のディレクトリ内に一時的なファイルを生成したり削除したりすることがありました。

このようなファイルの動的な変更は、filepath.Walk が走査中に予期せぬファイルやディレクトリの出現・消失を検知し、テストが失敗する原因となっていました。これは、テストが外部環境(この場合は他のテストの副作用)に依存してしまい、再現性の低い「flaky test」(不安定なテスト)となる典型的な例です。

Issue 5863 はこの問題が具体的に報告されたもので、テストが os ディレクトリ内のファイルの出現・消失によって失敗するという内容でした。このコミットは、この不安定性を解消し、テストの信頼性を向上させることを目的としています。

前提知識の解説

1. path/filepath パッケージ

path/filepath パッケージは、Go言語でファイルパスを操作するためのユーティリティを提供します。OSに依存しないパス操作(結合、分割、クリーンアップなど)や、ファイルシステムを走査するための Walk 関数などが含まれます。

2. filepath.Walk 関数

filepath.Walk は、指定されたルートディレクトリからファイルシステムツリーを再帰的に走査する関数です。走査中に見つかった各ファイルやディレクトリに対して、ユーザーが定義したコールバック関数(WalkFunc)を実行します。この関数は、ファイルシステムの状態を検査したり、特定のファイルを検索したりするのに非常に便利です。

filepath.Walk(root string, walkFn WalkFunc) error

  • root: 走査を開始するルートディレクトリのパス。
  • walkFn: 各ファイルまたはディレクトリに対して呼び出される関数。この関数は (path string, info os.FileInfo, err error) error というシグネチャを持ちます。
    • path: 現在走査中のファイルまたはディレクトリのパス。
    • info: そのファイルまたはディレクトリの os.FileInfo インターフェース(ファイル名、サイズ、パーミッション、更新時刻などの情報を含む)。
    • err: 走査中に発生したエラー。
    • walkFnfilepath.SkipDir を返すと、そのディレクトリの内容は走査されずにスキップされます。

3. runtime.GOROOT()

runtime.GOROOT() は、Go言語の標準ライブラリ runtime パッケージが提供する関数で、Goのインストールルートディレクトリの絶対パスを返します。このディレクトリには、Goのソースコード、標準ライブラリ、ツールなどが含まれています。

4. 競合状態 (Race Condition)

競合状態とは、複数の並行プロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。テストの文脈では、あるテストがファイルシステムを走査している最中に、別のテストが同じファイルシステム上のファイルを変更(作成、削除など)することで、走査結果が予期せぬものとなり、テストが失敗する現象がこれに該当します。これはテストの信頼性を著しく低下させます。

filepath.EvalSymlinks は、指定されたパス内のシンボリックリンクを評価し、最終的な物理パスを返します。これにより、シンボリックリンクをたどった後の実際のファイルシステム上の位置を取得できます。

技術的詳細

このコミットの技術的な核心は、テストの対象範囲を限定することで、テストの外部依存性を排除し、競合状態を解消することにあります。

元の TestBug3486 は、runtime.GOROOT() を起点としてファイルシステム全体を走査していました。これは、Goのインストールディレクトリ全体がテストの対象となり、その中には他のテストが一時的にファイルを操作する可能性のあるディレクトリ(例: os パッケージのテストが使用するディレクトリ)も含まれていました。

修正後のテストでは、filepath.Walk の走査開始パスを runtime.GOROOT() + "/test" に変更しています。GOROOT/test ディレクトリは、Goのテストスイート専用のファイルやディレクトリを含む場所であり、通常、他のテストがこのディレクトリ内のファイルを動的に変更することはありません。これにより、TestBug3486 が走査するファイルツリーが安定し、他のテストによる副作用の影響を受けなくなります。

また、テストの検証ロジックも変更されています。元々は libsrc ディレクトリが走査中に見られることを期待していましたが、修正後は bugsken というディレクトリが走査されることを期待しています。これらは GOROOT/test ディレクトリ内に存在するテスト用のサブディレクトリであり、テストの意図に合致しています。

さらに、filepath.Walk のコールバック関数内で filepath.SkipDir を使用して、特定のディレクトリ(bugs ディレクトリ)の走査をスキップするロジックが追加されています。これは、テストの効率化や、特定のサブツリーがテストの対象外であることを明示するために用いられます。また、ken ディレクトリが bugs ディレクトリの後に走査されることを確認する順序チェックも追加されており、filepath.Walk の走査順序に関する暗黙の仮定が明示的にテストされています。

この変更により、TestBug3486 はより隔離された環境で実行されるようになり、テストの信頼性と再現性が大幅に向上しました。

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

src/pkg/path/filepath/path_test.go ファイルが変更されています。

--- a/src/pkg/path/filepath/path_test.go
+++ b/src/pkg/path/filepath/path_test.go
@@ -927,27 +927,32 @@ func TestDriveLetterInEvalSymlinks(t *testing.T) {
 }
 
 func TestBug3486(t *testing.T) { // http://code.google.com/p/go/issues/detail?id=3486
-	root, err := filepath.EvalSymlinks(runtime.GOROOT())
+	root, err := filepath.EvalSymlinks(runtime.GOROOT() + "/test")
 	if err != nil {
 		t.Fatal(err)
 	}
-	lib := filepath.Join(root, "lib")
-	src := filepath.Join(root, "src")
-	seenSrc := false
+	bugs := filepath.Join(root, "bugs")
+	ken := filepath.Join(root, "ken")
+	seenBugs := false
+	seenKen := false
 	filepath.Walk(root, func(pth string, info os.FileInfo, err error) error {
 		if err != nil {
 			t.Fatal(err)
 		}
 
 		switch pth {
-		case lib:
+		case bugs:
+			seenBugs = true
 			return filepath.SkipDir
-		case src:
-			seenSrc = true
+		case ken:
+			if !seenBugs {
+				t.Fatal("filepath.Walk out of order - ken before bugs")
+			}
+			seenKen = true
 		}
 		return nil
 	})
-	if !seenSrc {
-		t.Fatalf("%q not seen", src)
+	if !seenKen {
+		t.Fatalf("%q not seen", ken)
 	}
 }

コアとなるコードの解説

  1. 走査ルートの変更:

    • 変更前: root, err := filepath.EvalSymlinks(runtime.GOROOT())
      • Goのインストールルートディレクトリ全体を走査対象としていました。
    • 変更後: root, err := filepath.EvalSymlinks(runtime.GOROOT() + "/test")
      • 走査対象を GOROOT 内の test ディレクトリに限定しました。このディレクトリはGoのテストスイート専用のファイルを含んでおり、他のテストによる干渉が少ないと期待されます。
  2. 期待するディレクトリパスの変更:

    • 変更前: lib := filepath.Join(root, "lib")src := filepath.Join(root, "src")
      • GOROOT 内の libsrc ディレクトリの存在を確認していました。
    • 変更後: bugs := filepath.Join(root, "bugs")ken := filepath.Join(root, "ken")
      • GOROOT/test 内の bugsken ディレクトリの存在を確認するように変更されました。これらはテスト専用のディレクトリです。
  3. filepath.Walk コールバックロジックの変更:

    • 変更前は lib ディレクトリで filepath.SkipDir を返し、src ディレクトリが見られたら seenSrctrue にしていました。
    • 変更後:
      • case bugs:: seenBugs = true とし、filepath.SkipDir を返して bugs ディレクトリ以下の走査をスキップします。
      • case ken:: seenKen = true とし、さらに !seenBugs であれば t.Fatal("filepath.Walk out of order - ken before bugs") を呼び出してテストを失敗させます。これは、filepath.Walk がディレクトリを走査する順序(通常は辞書順)を暗黙的にテストしています。bugsken より前に走査されることを期待しています。
  4. 最終的な検証の変更:

    • 変更前: if !seenSrc { t.Fatalf("%q not seen", src) }
      • src ディレクトリが見られたことを検証していました。
    • 変更後: if !seenKen { t.Fatalf("%q not seen", ken) }
      • ken ディレクトリが見られたことを検証するように変更されました。

これらの変更により、テストはより限定された、他のテストの副作用を受けにくいファイルツリーを対象とするようになり、テストの信頼性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語 path/filepath パッケージドキュメント: https://pkg.go.dev/path/filepath
  • Go言語 runtime パッケージドキュメント: https://pkg.go.dev/runtime
  • Go言語のテストに関するドキュメント (Goの公式ドキュメントやブログ記事など、一般的なテストのベストプラクティスに関する情報源)
  • 競合状態 (Race Condition) に関する一般的な情報源 (例: Wikipedia, プログラミング関連の技術ブログなど)
  • Go言語のソースコードリポジトリ (GitHub): https://github.com/golang/go
  • Go言語のIssueトラッカー (GitHub Issues): https://github.com/golang/go/issues