[インデックス 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
というサフィックスを持つファイルに記述され、TestXxx
、BenchmarkXxx
、ExampleXxx
といった命名規則に従います。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
フラグを受け取るための新しいグローバル変数outputDir
がflag.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つのファイルが変更されています。
src/cmd/go/doc.go
:go test
コマンドのドキュメントに-outputdir
フラグの説明を追加。src/cmd/go/test.go
:go test
コマンドのヘルプメッセージに-outputdir
フラグの説明を追加(doc.go
と同様の内容)。src/cmd/go/testflag.go
:-outputdir
フラグの定義を追加。- プロファイルフラグが指定され、かつ
-outputdir
が未指定の場合に、現在の作業ディレクトリを-test.outputdir
としてテストバイナリに渡すロジックを追加。
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
関数の一部です。
testProfile
が true
(つまり、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
をそのまま返します。 - 上記以外の場合(相対パスの場合)、
*outputDir
とpath
をシステム固有のパス区切り文字 (os.PathSeparator
) で結合し、新しい絶対パスを生成して返します。これにより、プロファイルファイルはoutputDir
で指定されたディレクトリに相対パスで指定されたファイル名で作成されることになります。
関連リンク
- Go言語のテスト: https://go.dev/doc/code#testing
- Go言語のプロファイリング: https://go.dev/doc/diagnostics#profiling
runtime/pprof
パッケージ: https://pkg.go.dev/runtime/pprofflag
パッケージ: https://pkg.go.dev/flagos
パッケージ: https://pkg.go.dev/os
参考にした情報源リンク
- https://github.com/golang/go/commit/28a1c36d627f179001a9d7180f81d947e6ecdaaf
- https://golang.org/cl/10234044
- Go言語の公式ドキュメント (上記「関連リンク」に記載の各パッケージドキュメント)
- Go言語のテストとプロファイリングに関する一般的な情報源 (Stack Overflow, ブログ記事など)# [インデックス 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言語のテストツール go test
でプロファイリング(CPUプロファイル、メモリプロファイル、ブロックプロファイルなど)を実行した際に、生成されるプロファイルファイルが常にテスト対象のパッケージのソースディレクトリに書き込まれてしまうという、ユーザーにとって不便で直感的ではない挙動を修正することにありました。
従来の挙動では、ユーザーが go test
を実行したカレントディレクトリとは異なる場所にプロファイルファイルが生成されるため、ファイルの管理が煩雑になったり、意図しない場所にファイルが作成されたりする問題がありました。特に、CI/CD環境や一時的なテスト実行の場合、ソースディレクトリを汚染することなく、一時的な出力ディレクトリにプロファイルファイルを生成したいというニーズがありました。
このコミットは、この「不明瞭な誤った挙動 (Obscure misfeature)」を修正し、ユーザーが go test
を実行したカレントディレクトリ、または明示的に指定したディレクトリにプロファイルファイルが出力されるようにすることで、より柔軟で予測可能なプロファイリング体験を提供することを目的としています。
また、testing
パッケージの after
関数(テスト実行後にクリーンアップやプロファイル出力を行う関数)内で発生したエラーが、詳細出力モード (-v
フラグ) でない限り報告されないという、エラーハンドリングの不備も同時に修正されています。これにより、プロファイルファイルの書き込み失敗などの重要なエラーが見過ごされるリスクが低減されました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とツールに関する知識が必要です。
1. go test
コマンド
go test
は、Go言語のパッケージに含まれるテストコードを実行するためのコマンドです。Goのテストは、_test.go
というサフィックスを持つファイルに記述され、TestXxx
、BenchmarkXxx
、ExampleXxx
といった命名規則に従います。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
フラグを受け取るための新しいグローバル変数outputDir
がflag.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つのファイルが変更されています。
src/cmd/go/doc.go
:go test
コマンドのドキュメントに-outputdir
フラグの説明を追加。src/cmd/go/test.go
:go test
コマンドのヘルプメッセージに-outputdir
フラグの説明を追加(doc.go
と同様の内容)。src/cmd/go/testflag.go
:-outputdir
フラグの定義を追加。- プロファイルフラグが指定され、かつ
-outputdir
が未指定の場合に、現在の作業ディレクトリを-test.outputdir
としてテストバイナリに渡すロジックを追加。
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
関数の一部です。
testProfile
が true
(つまり、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
をそのまま返します。 - 上記以外の場合(相対パスの場合)、
*outputDir
とpath
をシステム固有のパス区切り文字 (os.PathSeparator
) で結合し、新しい絶対パスを生成して返します。これにより、プロファイルファイルはoutputDir
で指定されたディレクトリに相対パスで指定されたファイル名で作成されることになります。
関連リンク
- Go言語のテスト: https://go.dev/doc/code#testing
- Go言語のプロファイリング: https://go.dev/doc/diagnostics#profiling
runtime/pprof
パッケージ: https://pkg.go.dev/runtime/pprofflag
パッケージ: https://pkg.go.dev/flagos
パッケージ: https://pkg.go.dev/os
参考にした情報源リンク
- https://github.com/golang/go/commit/28a1c36d627f179001a9d7180f81d947e6ecdaaf
- https://golang.org/cl/10234044
- Go言語の公式ドキュメント (上記「関連リンク」に記載の各パッケージドキュメント)
- Go言語のテストとプロファイリングに関する一般的な情報源 (Stack Overflow, ブログ記事など)