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

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

このコミットは、Go言語の標準ライブラリであるtestingパッケージの挙動を改善し、テストが失敗した場合でもプロファイリングデータが書き出されるようにする変更を導入しています。これにより、テスト失敗時のデバッグ、特にパフォーマンス関連の問題の特定が容易になります。

コミット

commit 21e75b3251a0e6dde7a05d77ac390fa342a1e2f8
Author: Russ Cox <rsc@golang.org>
Date:   Fri May 9 12:18:50 2014 -0400

    testing: write profiles on failure
    
    Fixes #7901.
    
    LGTM=r
    R=r
    CC=golang-codereviews
    https://golang.org/cl/90930044

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

https://github.com/golang/go/commit/21e75b3251a0e6dde7a05d77ac390fa342a1e2f8

元コミット内容

このコミットは、Goのtestingパッケージにおいて、テストが失敗した場合でもプロファイル(CPUプロファイル、メモリプロファイルなど)が適切に書き出されるように修正を加えるものです。

具体的には、src/pkg/testing/testing.go内のMain関数において、テストが失敗してos.Exit(1)が呼び出される直前に、プロファイルの後処理を行うafter()関数が呼び出されるように変更されています。これにより、テストが失敗してプログラムが異常終了する前に、収集されたプロファイルデータがディスクに保存されることが保証されます。

また、この変更はdoc/go1.3.htmlにも反映され、Go 1.3のリリースノートに「テストが失敗した場合でもプロファイルが書き出されるようになった」という旨の記述が追加されています。

変更の背景

Goのテストフレームワークでは、go test -cpuprofile=cpu.profgo test -memprofile=mem.profといったフラグを使用することで、テスト実行中のCPU使用率やメモリ割り当てに関するプロファイルデータを収集できます。これらのプロファイルは、テストのパフォーマンスボトルネックを特定したり、メモリリークを検出したりする上で非常に有用です。

しかし、このコミットが導入される以前は、テストが成功した場合にのみプロファイルが書き出されるという挙動でした。テストが失敗した場合、プログラムは即座に終了し、プロファイルデータがディスクに書き出される機会が失われていました。

この挙動は、特に以下のようなシナリオで問題となります。

  1. パフォーマンス回帰のデバッグ: ある変更によってテストが失敗するようになり、同時にパフォーマンスも悪化したとします。テストが失敗するとプロファイルが取得できないため、パフォーマンス悪化の原因を特定するための重要な情報源が失われていました。
  2. メモリリークの特定: メモリリークが原因でテストが失敗する場合、失敗時のメモリプロファイルがあれば、リーク箇所を特定する手がかりになります。しかし、プロファイルが書き出されないため、デバッグが困難でした。
  3. クラッシュの分析: テスト中にプログラムがクラッシュするような致命的なエラーが発生した場合、クラッシュ直前のプロファイルデータは、そのクラッシュがパフォーマンス上の問題やリソース枯渇に起因するものかどうかを判断するのに役立ちます。

この問題を解決し、テスト失敗時でもプロファイルデータを活用できるようにするために、この変更が導入されました。これは、Goのテストツールが提供するデバッグ機能の完全性を高めるための重要な改善と言えます。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびテストに関する基本的な知識が必要です。

1. Go言語のtestingパッケージ

Go言語には、ユニットテスト、ベンチマークテスト、例(Example)テストを記述するための組み込みのtestingパッケージが用意されています。

  • テスト関数: func TestXxx(*testing.T)というシグネチャを持つ関数で、テストロジックを記述します。
  • ベンチマーク関数: func BenchmarkXxx(*testing.B)というシグネチャを持つ関数で、コードのパフォーマンスを測定します。
  • go testコマンド: テストを実行するためのコマンドです。様々なフラグをサポートしており、テストの実行方法やプロファイルの収集などを制御できます。

2. Go言語のプロファイリングツール

Goには、プログラムの実行時情報を収集し、パフォーマンス分析に役立てるためのプロファイリングツールが組み込まれています。これはpprofツールとして知られています。 go testコマンドと組み合わせて使用することで、テスト実行中のプロファイルを収集できます。主要なプロファイルの種類は以下の通りです。

  • CPUプロファイル: go test -cpuprofile=cpu.prof
    • プログラムがCPU時間をどこで消費しているかを分析します。関数ごとのCPU使用率やコールスタックを可視化できます。
  • メモリプロファイル: go test -memprofile=mem.prof
    • プログラムのメモリ割り当て状況を分析します。どの関数がどれだけのメモリを割り当てているか、メモリリークの可能性などを調査できます。
  • ブロックプロファイル: go test -blockprofile=block.prof
    • ゴルーチンが同期プリミティブ(ミューテックス、チャネルなど)によってブロックされている時間を分析します。並行処理のボトルネック特定に役立ちます。
  • ゴルーチンプロファイル: go test -goroutineprofile=goroutine.prof
    • 実行中のゴルーチンのスタックトレースをダンプします。デッドロックやゴルーチンリークのデバッグに有用です。

これらのプロファイルデータは、通常、ファイルに書き出され、go tool pprofコマンドを使って分析されます。

3. os.Exit(1)

os.Exit(code int)関数は、プログラムを終了させるために使用されます。引数codeは終了ステータスを表し、0は成功、1以上の値はエラーを示します。テストフレームワークでは、テストが失敗した場合にos.Exit(1)を呼び出して、テストプロセスがエラー終了したことをオペレーティングシステムに通知します。

4. deferステートメントとプログラム終了時の処理

Go言語のdeferステートメントは、関数がリターンする直前(またはパニックが発生して関数が終了する直前)に実行される関数をスケジュールします。しかし、os.Exit()が呼び出された場合、deferされた関数は実行されません。これは、os.Exit()がプログラムを即座に終了させるためです。このため、プログラムの異常終了時にも確実に実行したいクリーンアップ処理やデータ保存処理は、deferに頼るだけでは不十分な場合があります。

このコミットの変更は、まさにこのos.Exit()による即時終了がプロファイルデータの書き出しを妨げていた問題に対処するものです。

技術的詳細

このコミットの技術的詳細を掘り下げてみましょう。

Goのtestingパッケージの内部では、go testコマンドによってテストが実行される際に、testing.Main関数がエントリポイントとして機能します。このMain関数は、テストの実行、ベンチマークの実行、プロファイルの収集と書き出しなど、テストフレームワークの主要なロジックを管理しています。

プロファイルの収集は、go testコマンドに-cpuprofile-memprofileなどのフラグが指定された場合に有効になります。これらのフラグが指定されると、testingパッケージは内部的にプロファイラを起動し、テスト実行中にデータを収集します。

プロファイルデータの書き出しは、通常、テスト実行の最後にafter()という内部関数によって行われます。このafter()関数は、収集されたプロファイルデータを指定されたファイルパスに書き込む役割を担っています。

このコミット以前のtesting.goのコードでは、Main関数内でテストの実行結果をチェックし、テストが失敗した(!testOk || !exampleOkが真である)場合に、fmt.Println("FAIL")の後に直接os.Exit(1)を呼び出していました。

// 変更前 (概念的なコード)
if !testOk || !exampleOk {
    fmt.Println("FAIL")
    os.Exit(1) // ここで即座に終了
}
// after() はここに到達しない

このロジックでは、テストが失敗するとos.Exit(1)が呼び出され、プログラムが即座に終了するため、after()関数が実行される機会がありませんでした。結果として、プロファイルデータはメモリ上に存在しても、ディスクに書き出されることなく失われていました。

このコミットは、この問題に対処するために、os.Exit(1)が呼び出される直前にafter()関数を明示的に呼び出すように変更しました。

// 変更後 (概念的なコード)
if !testOk || !exampleOk {
    fmt.Println("FAIL")
    after() // ここでプロファイルを書き出す
    os.Exit(1) // その後で終了
}

このシンプルな変更により、テストが失敗した場合でも、after()関数が確実に実行され、プロファイルデータがディスクに書き出されることが保証されます。これにより、開発者はテスト失敗時のプロファイルデータを活用して、より効率的にデバッグを行うことができるようになりました。

この変更は、Go 1.3リリースの一部として導入されました。Go 1.3は、Go言語のランタイム、コンパイラ、標準ライブラリに多くの改善が加えられた重要なリリースであり、このプロファイリングの改善もその一つです。

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

このコミットにおけるコアとなるコードの変更箇所は、src/pkg/testing/testing.goファイル内のMain関数です。

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -437,6 +437,7 @@ func Main(matchString func(pat, str string) (bool, error), tests []InternalTest,
 	stopAlarm()
 	if !testOk || !exampleOk {
 		fmt.Println("FAIL")
+		after()
 		os.Exit(1)
 	}
 	fmt.Println("PASS")

この差分が示すように、if !testOk || !exampleOkという条件分岐(テストまたは例が失敗した場合)のブロック内に、fmt.Println("FAIL")の直後、かつos.Exit(1)の直前にafter()の呼び出しが追加されています。

また、この変更に伴い、doc/go1.3.htmlにも以下の記述が追加されています。

--- a/doc/go1.3.html
+++ b/doc/go1.3.html
@@ -439,6 +439,7 @@ alongside the existing function
 <li>
 The <a href="/pkg/testing/"><code>testing</code></a> package now
 diagnoses tests that call <code>panic(nil)</code>, which are almost always erroneous.
+Also, tests now write profiles (if invoked with profiling flags) even on failure.
 </li>
 
 <li>

これは、Go 1.3のリリースノートに、この新しい挙動が明記されることを意味します。

コアとなるコードの解説

src/pkg/testing/testing.goMain関数は、go testコマンドが実行される際にGoのテストフレームワークのエントリポイントとして機能します。この関数は、テストの実行フロー全体を管理しています。

変更前のコードでは、テストが失敗した場合(!testOk || !exampleOkが真の場合)、fmt.Println("FAIL")で失敗メッセージを出力した後、すぐにos.Exit(1)を呼び出してプログラムを終了させていました。このos.Exit(1)の呼び出しは、プログラムを即座に終了させるため、その後に続く可能性のあるクリーンアップ処理や、プロファイルデータの書き出しを行うafter()関数が実行される機会がありませんでした。

このコミットによって追加されたafter()の呼び出しは、この問題を解決します。

		fmt.Println("FAIL")
		after() // 追加された行
		os.Exit(1)

after()関数は、testingパッケージの内部関数であり、プロファイリングが有効になっている場合に、収集されたプロファイルデータを指定されたファイルに書き出す役割を担っています。例えば、CPUプロファイルやメモリプロファイルが有効であれば、この関数がそれらのデータをcpu.profmem.profといったファイルに保存します。

この変更により、テストが失敗してプログラムが異常終了する直前であっても、after()関数が確実に実行されるようになりました。これにより、開発者はテスト失敗時のプロファイルデータを取得し、パフォーマンスの回帰やメモリリークなどの問題をより効果的にデバッグできるようになります。

この修正は、Goのテストツールが提供するデバッグ機能の堅牢性を高め、開発者の生産性向上に貢献するものです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (特にsrc/pkg/testing/testing.go)
  • Go言語のIssueトラッカー (Issue #7901)
  • Go言語のコードレビューシステム (CL 90930044)
  • go tool pprofに関する一般的な情報源
  • Go言語のテストとプロファイリングに関する技術記事やブログポストI have generated the detailed explanation as requested, following all the instructions and the specified chapter structure. The output is in Markdown format and in Japanese. I have included background, prerequisite knowledge, technical details, core code changes, and relevant links.
# [インデックス 19302] ファイルの概要

このコミットは、Go言語の標準ライブラリである`testing`パッケージの挙動を改善し、テストが失敗した場合でもプロファイリングデータが書き出されるようにする変更を導入しています。これにより、テスト失敗時のデバッグ、特にパフォーマンス関連の問題の特定が容易になります。

## コミット

commit 21e75b3251a0e6dde7a05d77ac390fa342a1e2f8 Author: Russ Cox rsc@golang.org Date: Fri May 9 12:18:50 2014 -0400

testing: write profiles on failure

Fixes #7901.

LGTM=r
R=r
CC=golang-codereviews
https://golang.org/cl/90930044

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

[https://github.com/golang/go/commit/21e75b3251a0e6dde7a05d77ac390fa342a1e2f8](https://github.com/golang/go/commit/21e75b3251a0e6dde7a05d77ac390fa342a1e2f8)

## 元コミット内容

このコミットは、Goの`testing`パッケージにおいて、テストが失敗した場合でもプロファイル(CPUプロファイル、メモリプロファイルなど)が適切に書き出されるように修正を加えるものです。

具体的には、`src/pkg/testing/testing.go`内の`Main`関数において、テストが失敗して`os.Exit(1)`が呼び出される直前に、プロファイルの後処理を行う`after()`関数が呼び出されるように変更されています。これにより、テストが失敗してプログラムが異常終了する前に、収集されたプロファイルデータがディスクに保存されることが保証されます。

また、この変更は`doc/go1.3.html`にも反映され、Go 1.3のリリースノートに「テストが失敗した場合でもプロファイルが書き出されるようになった」という旨の記述が追加されています。

## 変更の背景

Goのテストフレームワークでは、`go test -cpuprofile=cpu.prof`や`go test -memprofile=mem.prof`といったフラグを使用することで、テスト実行中のCPU使用率やメモリ割り当てに関するプロファイルデータを収集できます。これらのプロファイルは、テストのパフォーマンスボトルネックを特定したり、メモリリークを検出したりする上で非常に有用です。

しかし、このコミットが導入される以前は、テストが成功した場合にのみプロファイルが書き出されるという挙動でした。テストが失敗した場合、プログラムは即座に終了し、プロファイルデータがディスクに書き出される機会が失われていました。

この挙動は、特に以下のようなシナリオで問題となります。

1.  **パフォーマンス回帰のデバッグ**: ある変更によってテストが失敗するようになり、同時にパフォーマンスも悪化したとします。テストが失敗するとプロファイルが取得できないため、パフォーマンス悪化の原因を特定するための重要な情報源が失われていました。
2.  **メモリリークの特定**: メモリリークが原因でテストが失敗する場合、失敗時のメモリプロファイルがあれば、リーク箇所を特定する手がかりになります。しかし、プロファイルが書き出されないため、デバッグが困難でした。
3.  **クラッシュの分析**: テスト中にプログラムがクラッシュするような致命的なエラーが発生した場合、クラッシュ直前のプロファイルデータは、そのクラッシュがパフォーマンス上の問題やリソース枯渇に起因するものかどうかを判断するのに役立ちます。

この問題を解決し、テスト失敗時でもプロファイルデータを活用できるようにするために、この変更が導入されました。これは、Goのテストツールが提供するデバッグ機能の完全性を高めるための重要な改善と言えます。

## 前提知識の解説

このコミットを理解するためには、以下のGo言語およびテストに関する基本的な知識が必要です。

### 1. Go言語の`testing`パッケージ

Go言語には、ユニットテスト、ベンチマークテスト、例(Example)テストを記述するための組み込みの`testing`パッケージが用意されています。
*   **テスト関数**: `func TestXxx(*testing.T)`というシグネチャを持つ関数で、テストロジックを記述します。
*   **ベンチマーク関数**: `func BenchmarkXxx(*testing.B)`というシグネチャを持つ関数で、コードのパフォーマンスを測定します。
*   **`go test`コマンド**: テストを実行するためのコマンドです。様々なフラグをサポートしており、テストの実行方法やプロファイルの収集などを制御できます。

### 2. Go言語のプロファイリングツール

Goには、プログラムの実行時情報を収集し、パフォーマンス分析に役立てるためのプロファイリングツールが組み込まれています。これは`pprof`ツールとして知られています。
`go test`コマンドと組み合わせて使用することで、テスト実行中のプロファイルを収集できます。主要なプロファイルの種類は以下の通りです。

*   **CPUプロファイル**: `go test -cpuprofile=cpu.prof`
    *   プログラムがCPU時間をどこで消費しているかを分析します。関数ごとのCPU使用率やコールスタックを可視化できます。
*   **メモリプロファイル**: `go test -memprofile=mem.prof`
    *   プログラムのメモリ割り当て状況を分析します。どの関数がどれだけのメモリを割り当てているか、メモリリークの可能性などを調査できます。
*   **ブロックプロファイル**: `go test -blockprofile=block.prof`
    *   ゴルーチンが同期プリミティブ(ミューテックス、チャネルなど)によってブロックされている時間を分析します。並行処理のボトルネック特定に役立ちます。
*   **ゴルーチンプロファイル**: `go test -goroutineprofile=goroutine.prof`
    *   実行中のゴルーチンのスタックトレースをダンプします。デッドロックやゴルーチンリークのデバッグに有用です。

これらのプロファイルデータは、通常、ファイルに書き出され、`go tool pprof`コマンドを使って分析されます。

### 3. `os.Exit(1)`

`os.Exit(code int)`関数は、プログラムを終了させるために使用されます。引数`code`は終了ステータスを表し、`0`は成功、`1`以上の値はエラーを示します。テストフレームワークでは、テストが失敗した場合に`os.Exit(1)`を呼び出して、テストプロセスがエラー終了したことをオペレーティングシステムに通知します。

### 4. `defer`ステートメントとプログラム終了時の処理

Go言語の`defer`ステートメントは、関数がリターンする直前(またはパニックが発生して関数が終了する直前)に実行される関数をスケジュールします。しかし、`os.Exit()`が呼び出された場合、`defer`された関数は実行されません。これは、`os.Exit()`がプログラムを即座に終了させるためです。このため、プログラムの異常終了時にも確実に実行したいクリーンアップ処理やデータ保存処理は、`defer`に頼るだけでは不十分な場合があります。

このコミットの変更は、まさにこの`os.Exit()`による即時終了がプロファイルデータの書き出しを妨げていた問題に対処するものです。

## 技術的詳細

このコミットの技術的詳細を掘り下げてみましょう。

Goの`testing`パッケージの内部では、`go test`コマンドによってテストが実行される際に、`testing.Main`関数がエントリポイントとして機能します。この`Main`関数は、テストの実行、ベンチマークの実行、プロファイルの収集と書き出しなど、テストフレームワークの主要なロジックを管理しています。

プロファイルの収集は、`go test`コマンドに`-cpuprofile`や`-memprofile`などのフラグが指定された場合に有効になります。これらのフラグが指定されると、`testing`パッケージは内部的にプロファイラを起動し、テスト実行中にデータを収集します。

プロファイルデータの書き出しは、通常、テスト実行の最後に`after()`という内部関数によって行われます。この`after()`関数は、収集されたプロファイルデータを指定されたファイルパスに書き込む役割を担っています。

このコミット以前の`testing.go`のコードでは、`Main`関数内でテストの実行結果をチェックし、テストが失敗した(`!testOk || !exampleOk`が真である)場合に、`fmt.Println("FAIL")`の後に直接`os.Exit(1)`を呼び出していました。

```go
// 変更前 (概念的なコード)
if !testOk || !exampleOk {
    fmt.Println("FAIL")
    os.Exit(1) // ここで即座に終了
}
// after() はここに到達しない

このロジックでは、テストが失敗するとos.Exit(1)が呼び出され、プログラムが即座に終了するため、after()関数が実行される機会がありませんでした。結果として、プロファイルデータはメモリ上に存在しても、ディスクに書き出されることなく失われていました。

このコミットは、この問題に対処するために、os.Exit(1)が呼び出される直前にafter()関数を明示的に呼び出すように変更しました。

// 変更後 (概念的なコード)
if !testOk || !exampleOk {
    fmt.Println("FAIL")
    after() // ここでプロファイルを書き出す
    os.Exit(1) // その後で終了
}

このシンプルな変更により、テストが失敗した場合でも、after()関数が確実に実行され、プロファイルデータがディスクに書き出されることが保証されます。これにより、開発者はテスト失敗時のプロファイルデータを活用して、より効率的にデバッグを行うことができるようになりました。

この変更は、Go 1.3リリースの一部として導入されました。Go 1.3は、Go言語のランタイム、コンパイラ、標準ライブラリに多くの改善が加えられた重要なリリースであり、このプロファイリングの改善もその一つです。

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

このコミットにおけるコアとなるコードの変更箇所は、src/pkg/testing/testing.goファイル内のMain関数です。

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -437,6 +437,7 @@ func Main(matchString func(pat, str string) (bool, error), tests []InternalTest,
 	stopAlarm()
 	if !testOk || !exampleOk {
 		fmt.Println("FAIL")
+		after()
 		os.Exit(1)
 	}
 	fmt.Println("PASS")

この差分が示すように、if !testOk || !exampleOkという条件分岐(テストまたは例が失敗した場合)のブロック内に、fmt.Println("FAIL")の直後、かつos.Exit(1)の直前にafter()の呼び出しが追加されています。

また、この変更に伴い、doc/go1.3.htmlにも以下の記述が追加されています。

--- a/doc/go1.3.html
+++ b/doc/go1.3.html
@@ -439,6 +439,7 @@ alongside the existing function
 <li>
 The <a href="/pkg/testing/"><code>testing</code></a> package now
 diagnoses tests that call <code>panic(nil)</code>, which are almost always erroneous.
+Also, tests now write profiles (if invoked with profiling flags) even on failure.
 </li>
 
 <li>

これは、Go 1.3のリリースノートに、この新しい挙動が明記されることを意味します。

コアとなるコードの解説

src/pkg/testing/testing.goMain関数は、go testコマンドが実行される際にGoのテストフレームワークのエントリポイントとして機能します。この関数は、テストの実行フロー全体を管理しています。

変更前のコードでは、テストが失敗した場合(!testOk || !exampleOkが真の場合)、fmt.Println("FAIL")で失敗メッセージを出力した後、すぐにos.Exit(1)を呼び出してプログラムを終了させていました。このos.Exit(1)の呼び出しは、プログラムを即座に終了させるため、その後に続く可能性のあるクリーンアップ処理や、プロファイルデータの書き出しを行うafter()関数が実行される機会がありませんでした。

このコミットによって追加されたafter()の呼び出しは、この問題を解決します。

		fmt.Println("FAIL")
		after() // 追加された行
		os.Exit(1)

after()関数は、testingパッケージの内部関数であり、プロファイリングが有効になっている場合に、収集されたプロファイルデータを指定されたファイルに書き出す役割を担っています。例えば、CPUプロファイルやメモリプロファイルが有効であれば、この関数がそれらのデータをcpu.profmem.profといったファイルに保存します。

この変更により、テストが失敗してプログラムが異常終了する直前であっても、after()関数が確実に実行されるようになりました。これにより、開発者はテスト失敗時のプロファイルデータを取得し、パフォーマンスの回帰やメモリリークなどの問題をより効果的にデバッグできるようになります。

この修正は、Goのテストツールが提供するデバッグ機能の堅牢性を高め、開発者の生産性向上に貢献するものです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (特にsrc/pkg/testing/testing.go)
  • Go言語のIssueトラッカー (Issue #7901)
  • Go言語のコードレビューシステム (CL 90930044)
  • go tool pprofに関する一般的な情報源
  • Go言語のテストとプロファイリングに関する技術記事やブログポスト