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

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

このコミットは、Go言語のテストツール go test において、プロファイリング出力ファイルの保存先を制御するための -outputdir フラグを追加し、関連するバグを修正するものです。これにより、プロファイルデータが常にパッケージのソースディレクトリに書き込まれるという「不明瞭な誤った挙動」が修正され、go test コマンドが実行されたディレクトリにプロファイルファイルが出力されるようになります。また、testing.after 関数内で発生したエラーが -v フラグが設定されていないと報告されないという問題も修正されています。

コミット

commit 28a1c36d627f179001a9d7180f81d947e6ecdaaf
Author: Rob Pike <r@golang.org>
Date:   Wed Jun 12 18:13:34 2013 -0700

    testing: add -outputdir flag so "go test" controls where the files are written
    Obscure misfeature now fixed: When run from "go test", profiles were always
    written in the package's source directory. This change puts them in the directory
    where "go test" is run.
    Also fix a couple of problems causing errors in testing.after to go unreported
    unless -v was set.
    
    R=rsc, minux.ma, iant, alex.brainman
    CC=golang-dev
    https://golang.org/cl/10234044

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

https://github.com/golang/go/commit/28a1c36d627f179001a9d7180f81d947e6ecdaaf

元コミット内容

testing: add -outputdir flag so "go test" controls where the files are written Obscure misfeature now fixed: When run from "go test", profiles were always written in the package's source directory. This change puts them in the directory where "go test" is run. Also fix a couple of problems causing errors in testing.after to go unreported unless -v was set.

変更の背景

この変更の主な背景は、go test コマンドでプロファイリング(CPUプロファイル、メモリプロファイル、ブロックプロファイルなど)を実行した際に、生成されるプロファイルファイルが常にテスト対象のパッケージのソースディレクトリに書き込まれてしまうという、ユーザーにとって不便で直感的ではない挙動を修正することにありました。

従来の挙動では、ユーザーが go test を実行したカレントディレクトリとは異なる場所にプロファイルファイルが生成されるため、ファイルの管理が煩雑になったり、意図しない場所にファイルが作成されたりする問題がありました。特に、CI/CD環境や一時的なテスト実行の場合、ソースディレクトリを汚染することなく、一時的な出力ディレクトリにプロファイルファイルを生成したいというニーズがありました。

このコミットは、この「不明瞭な誤った挙動 (Obscure misfeature)」を修正し、ユーザーが go test を実行したカレントディレクトリ、または明示的に指定したディレクトリにプロファイルファイルが出力されるようにすることで、より柔軟で予測可能なプロファイリング体験を提供することを目的としています。

また、testing パッケージの after 関数(テスト実行後にクリーンアップやプロファイル出力を行う関数)内で発生したエラーが、詳細出力モード (-v フラグ) でない限り報告されないという、エラーハンドリングの不備も同時に修正されています。これにより、プロファイルファイルの書き込み失敗などの重要なエラーが見過ごされるリスクが低減されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。

1. go test コマンド

go test は、Go言語のパッケージに含まれるテストコードを実行するためのコマンドです。Goのテストは、_test.go というサフィックスを持つファイルに記述され、TestXxxBenchmarkXxxExampleXxx といった命名規則に従います。go test はこれらのテスト関数を自動的に発見し、実行します。

2. プロファイリング (Profiling)

プロファイリングは、プログラムの実行中にそのパフォーマンス特性(CPU使用率、メモリ割り当て、ゴルーチンブロックなど)を測定・分析する手法です。Go言語には、標準ライブラリの runtime/pprof パッケージを通じて強力なプロファイリング機能が組み込まれています。

  • CPUプロファイル: プログラムがCPU時間をどこで消費しているかを特定します。
  • メモリプロファイル: プログラムがメモリをどのように割り当て、使用しているかを分析します。
  • ブロックプロファイル: ゴルーチンが同期プリミティブ(ミューテックス、チャネルなど)によってブロックされている時間を測定し、並行処理のボトルネックを特定します。

go test コマンドは、これらのプロファイルを生成するためのフラグを提供しています。例えば、-cpuprofile-memprofile-blockprofile などです。これらのフラグを指定すると、テスト実行後に指定されたファイルにプロファイルデータが書き込まれます。

3. runtime/pprof パッケージ

runtime/pprof パッケージは、GoプログラムのプロファイリングデータにアクセスするためのAPIを提供します。pprof.StartCPUProfile()pprof.WriteHeapProfile()pprof.Lookup("block").WriteTo() などの関数を使用して、プロファイルデータを収集し、ファイルに書き出すことができます。

4. flag パッケージ

Goの標準ライブラリである flag パッケージは、コマンドライン引数を解析するための機能を提供します。go test コマンドの -outputdir のようなフラグは、このパッケージを使用して定義され、プログラム内でその値にアクセスできるようになります。

5. os パッケージ

os パッケージは、オペレーティングシステムと対話するための機能を提供します。このコミットでは、以下の関数が重要です。

  • os.Getwd(): 現在の作業ディレクトリの絶対パスを返します。
  • os.Create(): 指定されたパスに新しいファイルを作成します。
  • os.IsPathSeparator(): 指定された文字がパス区切り文字(例: /\)であるかどうかを判定します。
  • os.PathSeparator: オペレーティングシステム固有のパス区切り文字を表す定数です。

6. runtime.GOOS

runtime.GOOS は、Goプログラムが実行されているオペレーティングシステムの名前(例: "linux", "windows", "darwin")を表す文字列定数です。このコミットでは、Windows環境でのパスの扱いを特別に処理するために使用されています。

技術的詳細

このコミットの技術的な詳細は、主に以下の3つの領域に分けられます。

1. -outputdir フラグの導入

  • src/cmd/go/doc.go および src/cmd/go/test.go: go test コマンドのヘルプメッセージに -outputdir フラグの説明が追加されました。これにより、ユーザーはこの新しいフラグの存在と目的を認識できるようになります。
    +	-outputdir directory
    +	    Place output files from profiling in the specified directory,
    +	    by default the directory in which "go test" is running.
    
  • src/cmd/go/testflag.go:
    • testFlagDefn-outputdir フラグが追加され、passToTest: true が設定されています。これは、go test コマンドが受け取った -outputdir の値を、実際にテストを実行するバイナリ(go test がビルドして実行するテストプログラム)に -test.outputdir として渡すことを意味します。
    • testFlags 関数内で、プロファイルフラグ(-cpuprofile, -memprofile, -blockprofile)が指定されており、かつ -outputdir が明示的に指定されていない場合に、現在の作業ディレクトリ (os.Getwd()) を取得し、そのパスを -test.outputdir としてテストバイナリに渡すロジックが追加されました。これにより、デフォルトでプロファイルファイルが go test 実行ディレクトリに出力されるようになります。
      	// Tell the test what directory we're running in, so it can write the profiles there.
      	if testProfile && outputDir == "" {
      		dir, err := os.Getwd()
      		if err != nil {
      			fatalf("error from os.Getwd: %s", err)
      		}
      		passToTest = append(passToTest, "-test.outputdir", dir)
      	}
      

2. testing パッケージでの出力ディレクトリの利用

  • src/pkg/testing/testing.go:
    • testing パッケージ内に、go test から渡される -test.outputdir フラグを受け取るための新しいグローバル変数 outputDirflag.String を使って定義されました。
      	// The directory in which to create profile files and the like. When run from
      	// "go test", the binary always runs in the source directory for the package;
      	// this flag lets "go test" tell the binary to write the files in the directory where
      	// the "go test" command is run.
      	outputDir = flag.String("test.outputdir", "", "directory in which to write profiles")
      
    • プロファイルファイルを生成する before() および after() 関数内で、os.Create() を呼び出す際に、新しいヘルパー関数 toOutputDir() を介してファイルパスが処理されるようになりました。これにより、プロファイルファイルが outputDir で指定されたディレクトリに書き込まれるようになります。

3. toOutputDir ヘルパー関数の導入とパスの正規化

  • src/pkg/testing/testing.go: 新しい内部ヘルパー関数 toOutputDir が追加されました。この関数は、プロファイルファイルのパスを outputDir に基づいて調整します。
    // toOutputDir returns the file name relocated, if required, to outputDir.
    // Simple implementation to avoid pulling in path/filepath.
    func toOutputDir(path string) string {
    	if *outputDir == "" || path == "" {
    		return path
    	}
    	if runtime.GOOS == "windows" {
    		// On Windows, it's clumsy, but we can be almost always correct
    		// by just looking for a drive letter and a colon.
    		// Absolute paths always have a drive letter (ignoring UNC).
    		// Problem: if path == "C:A" and outputdir == "C:\Go" it's unclear
    		// what to do, but even then path/filepath doesn't help.
    		// TODO: Worth doing better? Probably not, because we're here only
    		// under the management of go test.
    		if len(path) >= 2 {
    			letter, colon := path[0], path[1]
    			if ('a' <= letter && letter <= 'z' || 'A' <= letter && letter <= 'Z') && colon == ':' {
    				// If path starts with a drive letter we're stuck with it regardless.
    				return path
    			}
    		}
    	}
    	if os.IsPathSeparator(path[0]) {
    		return path
    	}
    	return fmt.Sprintf("%s%c%s", *outputDir, os.PathSeparator, path)
    }
    
    • この関数は、outputDir が空でない場合にのみパスを調整します。
    • Windows環境では、ドライブレター(例: C:)で始まる絶対パスの場合、パスをそのまま返す特殊な処理が行われます。これは、path/filepath パッケージをインポートせずに、単純な文字列操作で対応するためです。
    • パスが絶対パス(パス区切り文字で始まる)である場合も、そのまま返されます。
    • それ以外の場合(相対パスの場合)、outputDir とシステム固有のパス区切り文字を結合して、新しい絶対パスを構築します。

4. エラー報告の改善

  • src/pkg/testing/testing.go: after() 関数内でプロファイルファイルの作成や書き込みに失敗した場合のエラーハンドリングが改善されました。以前は fmt.Fprintf(os.Stderr, ...) でエラーを出力するだけで、テストの終了ステータスに影響を与えませんでしたが、この変更により os.Exit(2) が呼び出されるようになり、エラーが発生した際にテストプロセスが非ゼロの終了コードで終了するようになりました。これにより、CI/CDシステムなどでエラーを検知しやすくなります。
    -			fmt.Fprintf(os.Stderr, "testing: %s", err)
    -			return
    +			fmt.Fprintf(os.Stderr, "testing: %s\n", err)
    +			os.Exit(2)
    

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

このコミットでは、以下の4つのファイルが変更されています。

  1. src/cmd/go/doc.go: go test コマンドのドキュメントに -outputdir フラグの説明を追加。
  2. src/cmd/go/test.go: go test コマンドのヘルプメッセージに -outputdir フラグの説明を追加(doc.go と同様の内容)。
  3. src/cmd/go/testflag.go:
    • -outputdir フラグの定義を追加。
    • プロファイルフラグが指定され、かつ -outputdir が未指定の場合に、現在の作業ディレクトリを -test.outputdir としてテストバイナリに渡すロジックを追加。
  4. src/pkg/testing/testing.go:
    • -test.outputdir フラグを受け取るための outputDir 変数を定義。
    • プロファイルファイルの作成時に toOutputDir ヘルパー関数を使用してパスを調整するように変更。
    • プロファイルファイルの書き込みエラー時に os.Exit(2) を呼び出すように変更し、エラー報告を改善。
    • toOutputDir ヘルパー関数を新規追加。

コアとなるコードの解説

src/cmd/go/testflag.go の変更点

@@ -170,6 +173,8 @@ func testFlags(args []string) (packageNames, passToTest []string) {
 		case "blockprofile", "cpuprofile", "memprofile":
 			testProfile = true
 		case "outputdir":
 			outputDir = value
 		case "cover":
 			switch value {
 			case "set", "count", "atomic":
@@ -185,6 +190,14 @@ func testFlags(args []string) (packageNames, passToTest []string) {
 			passToTest = append(passToTest, "-test."+f.name+"="+value)
 		}
 	}
+	// Tell the test what directory we're running in, so it can write the profiles there.
+	if testProfile && outputDir == "" {
+		dir, err := os.Getwd()
+		if err != nil {
+			fatalf("error from os.Getwd: %s", err)
+		}
+		passToTest = append(passToTest, "-test.outputdir", dir)
+	}
 	return
 }

このスニペットは、go test コマンドが受け取った引数を解析する testFlags 関数の一部です。 testProfiletrue(つまり、CPU、メモリ、またはブロックプロファイルが要求されている)であり、かつ outputDir が空文字列(-outputdir フラグが明示的に指定されていない)の場合に、現在の作業ディレクトリ (os.Getwd()) を取得し、そのパスを -test.outputdir フラグの値として、テストバイナリに渡す passToTest スライスに追加しています。これにより、ユーザーが -outputdir を指定しなくても、プロファイルファイルは go test を実行したディレクトリにデフォルトで出力されるようになります。

src/pkg/testing/testing.go の変更点

@@ -114,6 +114,12 @@ var (
 	// full test of the package.
 	short = flag.Bool("test.short", false, "run smaller test suite to save time")
 
+	// The directory in which to create profile files and the like. When run from
+	// "go test", the binary always runs in the source directory for the package;
+	// this flag lets "go test" tell the binary to write the files in the directory where
+	// the "go test" command is run.
+	outputDir = flag.String("test.outputdir", "", "directory in which to write profiles")
+
 	// Report as tests are run; default is silent for success.
 	chatty           = flag.Bool("test.v", false, "verbose: print additional output")
 	match            = flag.String("test.run", "", "regular expression to select tests and examples to run")
@@ -466,7 +472,7 @@ func before() {
 		runtime.MemProfileRate = *memProfileRate
 	}
 	if *cpuProfile != "" {
-		f, err := os.Create(*cpuProfile)
+		f, err := os.Create(toOutputDir(*cpuProfile))
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "testing: %s", err)
 			return
@@ -489,29 +495,59 @@ func after() {
 		pprof.StopCPUProfile() // flushes profile to disk
 	}
 	if *memProfile != "" {
-		f, err := os.Create(*memProfile)
+		f, err := os.Create(toOutputDir(*memProfile))
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "testing: %s", err)
-			return
+			fmt.Fprintf(os.Stderr, "testing: %s\n", err)
+			os.Exit(2)
 		}
 		if err = pprof.WriteHeapProfile(f); err != nil {
-			fmt.Fprintf(os.Stderr, "testing: can't write %s: %s", *memProfile, err)
+			fmt.Fprintf(os.Stderr, "testing: can't write %s: %s\n", *memProfile, err)
+			os.Exit(2)
 		}
 		f.Close()
 	}
 	if *blockProfile != "" && *blockProfileRate >= 0 {
-		f, err := os.Create(*blockProfile)
+		f, err := os.Create(toOutputDir(*blockProfile))
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "testing: %s", err)
-			return
+			fmt.Fprintf(os.Stderr, "testing: %s\n", err)
+			os.Exit(2)
 		}
 		if err = pprof.Lookup("block").WriteTo(f, 0); err != nil {
-			fmt.Fprintf(os.Stderr, "testing: can't write %s: %s", *blockProfile, err)
+			fmt.Fprintf(os.Stderr, "testing: can't write %s: %s\n", *blockProfile, err)
+			os.Exit(2)
 		}
 		f.Close()
 	}
 }

このスニペットでは、まず outputDir という flag.String 型の変数が定義され、-test.outputdir フラグの値を受け取ります。 そして、before() および after() 関数内で、CPUプロファイル、メモリプロファイル、ブロックプロファイルなどのファイルを作成する際に、os.Create() の引数として直接ファイル名を与えるのではなく、toOutputDir() 関数を介してパスを渡すように変更されています。これにより、プロファイルファイルは outputDir で指定されたディレクトリに作成されるようになります。 また、プロファイルファイルの作成や書き込みでエラーが発生した場合に、os.Exit(2) を呼び出すことで、テストプロセスがエラー終了するようになり、エラーの可視性が向上しています。

toOutputDir 関数の新規追加

+// toOutputDir returns the file name relocated, if required, to outputDir.
+// Simple implementation to avoid pulling in path/filepath.
+func toOutputDir(path string) string {
+	if *outputDir == "" || path == "" {
+		return path
+	}
+	if runtime.GOOS == "windows" {
+		// On Windows, it's clumsy, but we can be almost always correct
+		// by just looking for a drive letter and a colon.
+		// Absolute paths always have a drive letter (ignoring UNC).
+		// Problem: if path == "C:A" and outputdir == "C:\Go" it's unclear
+		// what to do, but even then path/filepath doesn't help.
+		// TODO: Worth doing better? Probably not, because we're here only
+		// under the management of go test.
+		if len(path) >= 2 {
+			letter, colon := path[0], path[1]
+			if ('a' <= letter && letter <= 'z' || 'A' <= letter && letter <= 'Z') && colon == ':' {
+				// If path starts with a drive letter we're stuck with it regardless.
+				return path
+			}
+		}
+	}
+	if os.IsPathSeparator(path[0]) {
+		return path
+	}
+	return fmt.Sprintf("%s%c%s", *outputDir, os.PathSeparator, path)
+}

この toOutputDir 関数は、プロファイルファイルの出力パスを決定する中心的なロジックです。

  • outputDir が空の場合や path が空の場合は、そのまま path を返します。
  • Windows環境では、パスがドライブレター(例: C:)で始まる絶対パスであるかをチェックし、その場合は path をそのまま返します。これは、Windowsのパスの特殊性を考慮したもので、path/filepath パッケージへの依存を避けるための簡略化された実装です。
  • パスがシステム固有のパス区切り文字(/ または \)で始まる場合(つまり絶対パスの場合)も、path をそのまま返します。
  • 上記以外の場合(相対パスの場合)、*outputDirpath をシステム固有のパス区切り文字 (os.PathSeparator) で結合し、新しい絶対パスを生成して返します。これにより、プロファイルファイルは outputDir で指定されたディレクトリに相対パスで指定されたファイル名で作成されることになります。

関連リンク

参考にした情報源リンク

このコミットは、Go言語のテストツール go test において、プロファイリング出力ファイルの保存先を制御するための -outputdir フラグを追加し、関連するバグを修正するものです。これにより、プロファイルデータが常にパッケージのソースディレクトリに書き込まれるという「不明瞭な誤った挙動」が修正され、go test コマンドが実行されたディレクトリにプロファイルファイルが出力されるようになります。また、testing.after 関数内で発生したエラーが -v フラグが設定されていないと報告されないという問題も修正されています。

コミット

commit 28a1c36d627f179001a9d7180f81d947e6ecdaaf
Author: Rob Pike <r@golang.org>
Date:   Wed Jun 12 18:13:34 2013 -0700

    testing: add -outputdir flag so "go test" controls where the files are written
    Obscure misfeature now fixed: When run from "go test", profiles were always
    written in the package's source directory. This change puts them in the directory
    where "go test" is run.
    Also fix a couple of problems causing errors in testing.after to go unreported
    unless -v was set.
    
    R=rsc, minux.ma, iant, alex.brainman
    CC=golang-dev
    https://golang.org/cl/10234044

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

https://github.com/golang/go/commit/28a1c36d627f179001a9d7180f81d947e6ecdaaf

元コミット内容

testing: add -outputdir flag so "go test" controls where the files are written Obscure misfeature now fixed: When run from "go test", profiles were always written in the package's source directory. This change puts them in the directory where "go test" is run. Also fix a couple of problems causing errors in testing.after to go unreported unless -v was set.

変更の背景

この変更の主な背景は、Go言語のテストツール go test でプロファイリング(CPUプロファイル、メモリプロファイル、ブロックプロファイルなど)を実行した際に、生成されるプロファイルファイルが常にテスト対象のパッケージのソースディレクトリに書き込まれてしまうという、ユーザーにとって不便で直感的ではない挙動を修正することにありました。

従来の挙動では、ユーザーが go test を実行したカレントディレクトリとは異なる場所にプロファイルファイルが生成されるため、ファイルの管理が煩雑になったり、意図しない場所にファイルが作成されたりする問題がありました。特に、CI/CD環境や一時的なテスト実行の場合、ソースディレクトリを汚染することなく、一時的な出力ディレクトリにプロファイルファイルを生成したいというニーズがありました。

このコミットは、この「不明瞭な誤った挙動 (Obscure misfeature)」を修正し、ユーザーが go test を実行したカレントディレクトリ、または明示的に指定したディレクトリにプロファイルファイルが出力されるようにすることで、より柔軟で予測可能なプロファイリング体験を提供することを目的としています。

また、testing パッケージの after 関数(テスト実行後にクリーンアップやプロファイル出力を行う関数)内で発生したエラーが、詳細出力モード (-v フラグ) でない限り報告されないという、エラーハンドリングの不備も同時に修正されています。これにより、プロファイルファイルの書き込み失敗などの重要なエラーが見過ごされるリスクが低減されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。

1. go test コマンド

go test は、Go言語のパッケージに含まれるテストコードを実行するためのコマンドです。Goのテストは、_test.go というサフィックスを持つファイルに記述され、TestXxxBenchmarkXxxExampleXxx といった命名規則に従います。go test はこれらのテスト関数を自動的に発見し、実行します。

2. プロファイリング (Profiling)

プロファイリングは、プログラムの実行中にそのパフォーマンス特性(CPU使用率、メモリ割り当て、ゴルーチンブロックなど)を測定・分析する手法です。Go言語には、標準ライブラリの runtime/pprof パッケージを通じて強力なプロファイリング機能が組み込まれています。

  • CPUプロファイル: プログラムがCPU時間をどこで消費しているかを特定します。
  • メモリプロファイル: プログラムがメモリをどのように割り当て、使用しているかを分析します。
  • ブロックプロファイル: ゴルーチンが同期プリミティブ(ミューテックス、チャネルなど)によってブロックされている時間を測定し、並行処理のボトルネックを特定します。

go test コマンドは、これらのプロファイルを生成するためのフラグを提供しています。例えば、-cpuprofile-memprofile-blockprofile などです。これらのフラグを指定すると、テスト実行後に指定されたファイルにプロファイルデータが書き込まれます。

3. runtime/pprof パッケージ

runtime/pprof パッケージは、GoプログラムのプロファイリングデータにアクセスするためのAPIを提供します。pprof.StartCPUProfile()pprof.WriteHeapProfile()pprof.Lookup("block").WriteTo() などの関数を使用して、プロファイルデータを収集し、ファイルに書き出すことができます。

4. flag パッケージ

Goの標準ライブラリである flag パッケージは、コマンドライン引数を解析するための機能を提供します。go test コマンドの -outputdir のようなフラグは、このパッケージを使用して定義され、プログラム内でその値にアクセスできるようになります。

5. os パッケージ

os パッケージは、オペレーティングシステムと対話するための機能を提供します。このコミットでは、以下の関数が重要です。

  • os.Getwd(): 現在の作業ディレクトリの絶対パスを返します。
  • os.Create(): 指定されたパスに新しいファイルを作成します。
  • os.IsPathSeparator(): 指定された文字がパス区切り文字(例: /\)であるかどうかを判定します。
  • os.PathSeparator: オペレーティングシステム固有のパス区切り文字を表す定数です。

6. runtime.GOOS

runtime.GOOS は、Goプログラムが実行されているオペレーティングシステムの名前(例: "linux", "windows", "darwin")を表す文字列定数です。このコミットでは、Windows環境でのパスの扱いを特別に処理するために使用されています。

技術的詳細

このコミットの技術的な詳細は、主に以下の3つの領域に分けられます。

1. -outputdir フラグの導入

  • src/cmd/go/doc.go および src/cmd/go/test.go: go test コマンドのヘルプメッセージに -outputdir フラグの説明が追加されました。これにより、ユーザーはこの新しいフラグの存在と目的を認識できるようになります。
    +	-outputdir directory
    +	    Place output files from profiling in the specified directory,
    +	    by default the directory in which "go test" is running.
    
  • src/cmd/go/testflag.go:
    • testFlagDefn-outputdir フラグが追加され、passToTest: true が設定されています。これは、go test コマンドが受け取った -outputdir の値を、実際にテストを実行するバイナリ(go test がビルドして実行するテストプログラム)に -test.outputdir として渡すことを意味します。
    • testFlags 関数内で、プロファイルフラグ(-cpuprofile, -memprofile, -blockprofile)が指定されており、かつ -outputdir が明示的に指定されていない場合に、現在の作業ディレクトリ (os.Getwd()) を取得し、そのパスを -test.outputdir としてテストバイナリに渡すロジックが追加されました。これにより、デフォルトでプロファイルファイルが go test 実行ディレクトリに出力されるようになります。
      	// Tell the test what directory we're running in, so it can write the profiles there.
      	if testProfile && outputDir == "" {
      		dir, err := os.Getwd()
      		if err != nil {
      			fatalf("error from os.Getwd: %s", err)
      		}
      		passToTest = append(passToTest, "-test.outputdir", dir)
      	}
      

2. testing パッケージでの出力ディレクトリの利用

  • src/pkg/testing/testing.go:
    • testing パッケージ内に、go test から渡される -test.outputdir フラグを受け取るための新しいグローバル変数 outputDirflag.String を使って定義されました。
      	// The directory in which to create profile files and the like. When run from
      	// "go test", the binary always runs in the source directory for the package;
      	// this flag lets "go test" tell the binary to write the files in the directory where
      	// the "go test" command is run.
      	outputDir = flag.String("test.outputdir", "", "directory in which to write profiles")
      
    • プロファイルファイルを生成する before() および after() 関数内で、os.Create() を呼び出す際に、新しいヘルパー関数 toOutputDir() を介してファイルパスが処理されるようになりました。これにより、プロファイルファイルが outputDir で指定されたディレクトリに書き込まれるようになります。

3. toOutputDir ヘルパー関数の導入とパスの正規化

  • src/pkg/testing/testing.go: 新しい内部ヘルパー関数 toOutputDir が追加されました。この関数は、プロファイルファイルのパスを outputDir に基づいて調整します。
    // toOutputDir returns the file name relocated, if required, to outputDir.
    // Simple implementation to avoid pulling in path/filepath.
    func toOutputDir(path string) string {
    	if *outputDir == "" || path == "" {
    		return path
    	}
    	if runtime.GOOS == "windows" {
    		// On Windows, it's clumsy, but we can be almost always correct
    		// by just looking for a drive letter and a colon.
    		// Absolute paths always have a drive letter (ignoring UNC).
    		// Problem: if path == "C:A" and outputdir == "C:\Go" it's unclear
    		// what to do, but even then path/filepath doesn't help.
    		// TODO: Worth doing better? Probably not, because we're here only
    		// under the management of go test.
    		if len(path) >= 2 {
    			letter, colon := path[0], path[1]
    			if ('a' <= letter && letter <= 'z' || 'A' <= letter && letter <= 'Z') && colon == ':' {
    				// If path starts with a drive letter we're stuck with it regardless.
    				return path
    			}
    		}
    	}
    	if os.IsPathSeparator(path[0]) {
    		return path
    	}
    	return fmt.Sprintf("%s%c%s", *outputDir, os.PathSeparator, path)
    }
    
    • この関数は、outputDir が空でない場合にのみパスを調整します。
    • Windows環境では、ドライブレター(例: C:)で始まる絶対パスの場合、パスをそのまま返す特殊な処理が行われます。これは、path/filepath パッケージをインポートせずに、単純な文字列操作で対応するためです。
    • パスが絶対パス(パス区切り文字で始まる)である場合も、そのまま返されます。
    • それ以外の場合(相対パスの場合)、outputDir とシステム固有のパス区切り文字を結合して、新しい絶対パスを構築します。

4. エラー報告の改善

  • src/pkg/testing/testing.go: after() 関数内でプロファイルファイルの作成や書き込みに失敗した場合のエラーハンドリングが改善されました。以前は fmt.Fprintf(os.Stderr, ...) でエラーを出力するだけで、テストの終了ステータスに影響を与えませんでしたが、この変更により os.Exit(2) が呼び出されるようになり、エラーが発生した際にテストプロセスが非ゼロの終了コードで終了するようになりました。これにより、CI/CDシステムなどでエラーを検知しやすくなります。
    -			fmt.Fprintf(os.Stderr, "testing: %s", err)
    -			return
    +			fmt.Fprintf(os.Stderr, "testing: %s\n", err)
    +			os.Exit(2)
    

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

このコミットでは、以下の4つのファイルが変更されています。

  1. src/cmd/go/doc.go: go test コマンドのドキュメントに -outputdir フラグの説明を追加。
  2. src/cmd/go/test.go: go test コマンドのヘルプメッセージに -outputdir フラグの説明を追加(doc.go と同様の内容)。
  3. src/cmd/go/testflag.go:
    • -outputdir フラグの定義を追加。
    • プロファイルフラグが指定され、かつ -outputdir が未指定の場合に、現在の作業ディレクトリを -test.outputdir としてテストバイナリに渡すロジックを追加。
  4. src/pkg/testing/testing.go:
    • -test.outputdir フラグを受け取るための outputDir 変数を定義。
    • プロファイルファイルの作成時に toOutputDir ヘルパー関数を使用してパスを調整するように変更。
    • プロファイルファイルの書き込みエラー時に os.Exit(2) を呼び出すように変更し、エラー報告を改善。
    • toOutputDir ヘルパー関数を新規追加。

コアとなるコードの解説

src/cmd/go/testflag.go の変更点

@@ -170,6 +173,8 @@ func testFlags(args []string) (packageNames, passToTest []string) {
 		case "blockprofile", "cpuprofile", "memprofile":
 			testProfile = true
 		case "outputdir":
 			outputDir = value
 		case "cover":
 			switch value {
 			case "set", "count", "atomic":
@@ -185,6 +190,14 @@ func testFlags(args []string) (packageNames, passToTest []string) {
 			passToTest = append(passToTest, "-test."+f.name+"="+value)
 		}
 	}
+	// Tell the test what directory we're running in, so it can write the profiles there.
+	if testProfile && outputDir == "" {
+		dir, err := os.Getwd()
+		if err != nil {
+			fatalf("error from os.Getwd: %s", err)
+		}
+		passToTest = append(passToTest, "-test.outputdir", dir)
+	}
 	return
 }

このスニペットは、go test コマンドが受け取った引数を解析する testFlags 関数の一部です。 testProfiletrue(つまり、CPU、メモリ、またはブロックプロファイルが要求されている)であり、かつ outputDir が空文字列(-outputdir フラグが明示的に指定されていない)の場合に、現在の作業ディレクトリ (os.Getwd()) を取得し、そのパスを -test.outputdir フラグの値として、テストバイナリに渡す passToTest スライスに追加しています。これにより、ユーザーが -outputdir を指定しなくても、プロファイルファイルは go test を実行したディレクトリにデフォルトで出力されるようになります。

src/pkg/testing/testing.go の変更点

@@ -114,6 +114,12 @@ var (
 	// full test of the package.
 	short = flag.Bool("test.short", false, "run smaller test suite to save time")
 
+	// The directory in which to create profile files and the like. When run from
+	// "go test", the binary always runs in the source directory for the package;
+	// this flag lets "go test" tell the binary to write the files in the directory where
+	// the "go test" command is run.
+	outputDir = flag.String("test.outputdir", "", "directory in which to write profiles")
+
 	// Report as tests are run; default is silent for success.
 	chatty           = flag.Bool("test.v", false, "verbose: print additional output")
 	match            = flag.String("test.run", "", "regular expression to select tests and examples to run")
@@ -466,7 +472,7 @@ func before() {
 		runtime.MemProfileRate = *memProfileRate
 	}
 	if *cpuProfile != "" {
-		f, err := os.Create(*cpuProfile)
+		f, err := os.Create(toOutputDir(*cpuProfile))
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "testing: %s", err)
 			return
@@ -489,29 +495,59 @@ func after() {
 		pprof.StopCPUProfile() // flushes profile to disk
 	}
 	if *memProfile != "" {
-		f, err := os.Create(*memProfile)
+		f, err := os.Create(toOutputDir(*memProfile))
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "testing: %s", err)
-			return
+			fmt.Fprintf(os.Stderr, "testing: %s\n", err)
+			os.Exit(2)
 		}
 		if err = pprof.WriteHeapProfile(f); err != nil {
-			fmt.Fprintf(os.Stderr, "testing: can't write %s: %s", *memProfile, err)
+			fmt.Fprintf(os.Stderr, "testing: can't write %s: %s\n", *memProfile, err)
+			os.Exit(2)
 		}
 		f.Close()
 	}
 	if *blockProfile != "" && *blockProfileRate >= 0 {
-		f, err := os.Create(*blockProfile)
+		f, err := os.Create(toOutputDir(*blockProfile))
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "testing: %s", err)
-			return
+			fmt.Fprintf(os.Stderr, "testing: %s\n", err)
+			os.Exit(2)
 		}
 		if err = pprof.Lookup("block").WriteTo(f, 0); err != nil {
-			fmt.Fprintf(os.Stderr, "testing: can't write %s: %s", *blockProfile, err)
+			fmt.Fprintf(os.Stderr, "testing: can't write %s: %s\n", *blockProfile, err)
+			os.Exit(2)
 		}
 		f.Close()
 	}
 }

このスニペットでは、まず outputDir という flag.String 型の変数が定義され、-test.outputdir フラグの値を受け取ります。 そして、before() および after() 関数内で、CPUプロファイル、メモリプロファイル、ブロックプロファイルなどのファイルを作成する際に、os.Create() の引数として直接ファイル名を与えるのではなく、toOutputDir() 関数を介してパスを渡すように変更されています。これにより、プロファイルファイルは outputDir で指定されたディレクトリに作成されるようになります。 また、プロファイルファイルの作成や書き込みでエラーが発生した場合に、os.Exit(2) を呼び出すことで、テストプロセスがエラー終了するようになり、エラーの可視性が向上しています。

toOutputDir 関数の新規追加

+// toOutputDir returns the file name relocated, if required, to outputDir.
+// Simple implementation to avoid pulling in path/filepath.
+func toOutputDir(path string) string {
+	if *outputDir == "" || path == "" {
+		return path
+	}
+	if runtime.GOOS == "windows" {
+		// On Windows, it's clumsy, but we can be almost always correct
+		// by just looking for a drive letter and a colon.
+		// Absolute paths always have a drive letter (ignoring UNC).
+		// Problem: if path == "C:A" and outputdir == "C:\Go" it's unclear
+		// what to do, but even then path/filepath doesn't help.
+		// TODO: Worth doing better? Probably not, because we're here only
+		// under the management of go test.
+		if len(path) >= 2 {
+			letter, colon := path[0], path[1]
+			if ('a' <= letter && letter <= 'z' || 'A' <= letter && letter <= 'Z') && colon == ':' {
+				// If path starts with a drive letter we're stuck with it regardless.
+				return path
+			}
+		}
+	}
+	if os.IsPathSeparator(path[0]) {
+		return path
+	}
+	return fmt.Sprintf("%s%c%s", *outputDir, os.PathSeparator, path)
+}

この toOutputDir 関数は、プロファイルファイルの出力パスを決定する中心的なロジックです。

  • outputDir が空の場合や path が空の場合は、そのまま path を返します。
  • Windows環境では、パスがドライブレター(例: C:)で始まる絶対パスであるかをチェックし、その場合は path をそのまま返します。これは、Windowsのパスの特殊性を考慮したもので、path/filepath パッケージへの依存を避けるための簡略化された実装です。
  • パスがシステム固有のパス区切り文字(/ または \)で始まる場合(つまり絶対パスの場合)も、path をそのまま返します。
  • 上記以外の場合(相対パスの場合)、*outputDirpath をシステム固有のパス区切り文字 (os.PathSeparator) で結合し、新しい絶対パスを生成して返します。これにより、プロファイルファイルは outputDir で指定されたディレクトリに相対パスで指定されたファイル名で作成されることになります。

関連リンク

参考にした情報源リンク