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

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

このコミットは、Go言語の標準ライブラリである src/pkg/testing/testing.go に関連する変更です。testing パッケージは、Goプログラムの自動テストをサポートするための機能を提供します。具体的には、テスト関数、ベンチマーク関数、および例(Example)関数を記述し、実行するためのフレームワークです。このファイルは、go test コマンドがテストの実行、タイムアウト処理、結果の出力などをどのように管理するかを定義する、このパッケージの核心部分を構成しています。

コミット

  • コミットハッシュ: bd0a14fe4090ae2deb5dfe004ff5413ac259dc35
  • 作者: Dmitriy Vyukov dvyukov@google.com
  • コミット日時: 2013年8月1日 木曜日 17:24:24 +0400

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

https://github.com/golang/go/commit/bd0a14fe4090ae2deb5dfe004ff5413ac259dc35

元コミット内容

testing: say what was the timeout if it fires
It looks reasonable here and may be useful.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/12252043

変更の背景

このコミットが行われる前は、Goのテストがタイムアウトした場合、単に「test timed out」というメッセージが表示されるだけでした。これは、テストがどのくらいの時間でタイムアウトしたのか、つまり設定されたタイムアウト値が何であったのかが不明瞭であることを意味します。

開発者がテストのタイムアウトに遭遇した際、そのタイムアウトが意図したものであるか、あるいはテストが予期せず長時間実行された結果であるかを判断するためには、タイムアウト値を知ることが非常に重要です。例えば、テストが非常に長い処理を行うことが予想される場合、タイムアウト値を適切に設定しているかを確認したいことがあります。また、テストがデッドロックや無限ループに陥っている場合、タイムアウト値が短すぎるのか、それともテスト自体の問題なのかを切り分ける手がかりにもなります。

このコミットの目的は、タイムアウトが発生した際に、そのタイムアウトが設定された具体的な時間(例: 「test timed out after 30s」)をエラーメッセージに含めることで、デバッグの効率を向上させることにありました。これにより、開発者はタイムアウトの原因をより迅速に特定し、問題解決に役立てることができます。

前提知識の解説

Go言語の testing パッケージ

Go言語には、標準ライブラリとして testing パッケージが用意されており、ユニットテスト、ベンチマークテスト、および例(Example)テストをサポートします。

  • ユニットテスト: func TestXxx(*testing.T) という形式の関数で記述され、特定のコードユニットの動作を検証します。
  • ベンチマークテスト: func BenchmarkXxx(*testing.B) という形式の関数で記述され、コードのパフォーマンスを測定します。
  • 例(Example)テスト: func ExampleXxx() という形式の関数で記述され、コードの使用例を示し、その出力が期待通りであることを検証します。

go test コマンドは、これらのテスト関数を自動的に発見し、実行します。

go test -timeout オプション

go test コマンドには、-timeout オプションがあり、テスト全体の実行時間に上限を設定できます。例えば、go test -timeout 30s と指定すると、テストが30秒以内に完了しない場合、強制的に終了させられます。この機能は、無限ループやデッドロックなどによってテストがハングアップするのを防ぐために非常に重要です。

time.AfterFunc 関数

time パッケージの AfterFunc 関数は、指定された期間が経過した後に一度だけ関数を実行するためのタイマーを設定します。 time.AfterFunc(d Duration, f func()) *Timer この関数は、d で指定された期間が経過した後、ゴルーチン内で f を実行します。このコミットでは、タイムアウト期間が経過した後に panic を発生させるために使用されています。

panic 関数

Go言語の panic は、プログラムの異常終了を引き起こす組み込み関数です。通常、回復不可能なエラーや予期せぬ状況が発生した場合に使用されます。panic が発生すると、現在のゴルーチンの通常の実行フローが停止し、遅延関数(defer ステートメントで登録された関数)が実行され、その後、呼び出しスタックを遡ってパニックが伝播します。もし、スタックのどこにも recover がない場合、プログラムはクラッシュし、スタックトレースが出力されます。テストのタイムアウトにおいては、panic を発生させることで、テストの実行を強制的に終了させ、エラーメッセージを出力するメカニズムとして利用されます。

fmt.Sprintf 関数

fmt パッケージの Sprintf 関数は、フォーマットされた文字列を生成しますが、その結果を標準出力に出力するのではなく、文字列として返します。 func Sprintf(format string, a ...interface{}) string この関数は、C言語の sprintf に似ており、様々な型の値を指定されたフォーマット文字列に従って整形し、新しい文字列を作成するのに役立ちます。このコミットでは、タイムアウトメッセージにタイムアウト値を埋め込むために使用されています。

技術的詳細

このコミットの主要な変更点は、テストのタイムアウト処理におけるエラーメッセージの改善と、stopAlarm の呼び出し位置の調整です。

タイムアウトメッセージの改善

変更前は、startAlarm 関数内で time.AfterFunc が呼び出す alarm 関数が、単に panic("test timed out") を実行していました。 変更後は、alarm 関数が削除され、time.AfterFunc に直接無名関数が渡されるようになりました。この無名関数の中で、fmt.Sprintf を使用して、設定されたタイムアウト値 (*timeout) を含むより詳細なパニックメッセージが生成されます。

// 変更前
// func alarm() {
// 	panic("test timed out")
// }
// timer = time.AfterFunc(*timeout, alarm)

// 変更後
timer = time.AfterFunc(*timeout, func() {
	panic(fmt.Sprintf("test timed out after %v", *timeout))
})

この変更により、タイムアウトが発生した際に、例えば「test timed out after 30s」のような具体的なメッセージが表示されるようになり、デバッグ情報が格段に向上しました。

stopAlarm の呼び出し位置の変更

Main 関数は、テスト、例、ベンチマークの実行を調整するGoテストフレームワークのエントリポイントです。 変更前は、stopAlarm()fmt.Println("PASS") の後に呼び出されていました。これは、テストと例がすべて成功した場合にのみ実行されるパスです。 変更後は、stopAlarm() の呼び出しが testOk || !exampleOk の条件分岐の直前、つまりテストと例の実行が完了した直後に移動されました。

// 変更前
// if !testOk || !exampleOk {
// 	fmt.Println("FAIL")
// 	os.Exit(1)
// }
// fmt.Println("PASS")
// stopAlarm() // ここにあった

// 変更後
stopAlarm() // ここに移動
if !testOk || !exampleOk {
	fmt.Println("FAIL")
	os.Exit(1)
}
fmt.Println("PASS") // ここにはない

この変更の意図は、テストが成功してもしなくても、テストと例の実行が完了した時点で必ずタイマーを停止させるためと考えられます。これにより、ベンチマークの実行前にタイマーが確実に停止され、ベンチマークの実行がタイムアウトの影響を受けないように保証されます。また、テストが失敗して os.Exit(1) でプログラムが終了する場合でも、stopAlarm が呼び出される機会が増え、リソースのクリーンアップがより確実に行われるようになります。

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

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -371,12 +371,12 @@ func Main(matchString func(pat, str string) (bool, error), tests []InternalTest,
 	haveExamples = len(examples) > 0
 	testOk := RunTests(matchString, tests)
 	exampleOk := RunExamples(matchString, examples)
+\tstopAlarm()
 \tif !testOk || !exampleOk {\n \t\tfmt.Println(\"FAIL\")
 \t\tos.Exit(1)
 \t}\n \tfmt.Println(\"PASS\")
-\tstopAlarm()\n \tRunBenchmarks(matchString, benchmarks)
 \tafter()\n }\n@@ -561,7 +561,9 @@ var timer *time.Timer
 // startAlarm starts an alarm if requested.\n func startAlarm() {\n \tif *timeout > 0 {\n-\t\ttimer = time.AfterFunc(*timeout, alarm)\n+\t\ttimer = time.AfterFunc(*timeout, func() {\n+\t\t\tpanic(fmt.Sprintf(\"test timed out after %v\", *timeout))\n+\t\t})\n \t}\n }\n \n@@ -572,11 +574,6 @@ func stopAlarm() {\n \t}\n }\n \n-// alarm is called if the timeout expires.\n-func alarm() {\n-\tpanic(\"test timed out\")\n-}\n-\n func parseCpuList() {\n \tif len(*cpuListStr) == 0 {\n \t\tcpuList = append(cpuList, runtime.GOMAXPROCS(-1))\n```

## コアとなるコードの解説

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

1.  **`Main` 関数内の `stopAlarm()` の移動**:
    *   **変更前**: `stopAlarm()` は `fmt.Println("PASS")` の直後に呼び出されていました。これは、テストと例がすべて成功した場合にのみタイマーが停止されることを意味します。
    *   **変更後**: `stopAlarm()` は `RunExamples` の呼び出し直後、かつ `if !testOk || !exampleOk` の条件分岐の前に移動されました。これにより、テストと例の実行が完了した時点で、その成否にかかわらず必ずタイマーが停止されるようになりました。これは、ベンチマークの実行がタイムアウトの影響を受けないようにするため、およびテストが失敗した場合でもタイマーが適切にクリーンアップされるようにするために重要です。

2.  **`startAlarm` 関数内の `time.AfterFunc` のコールバック変更**:
    *   **変更前**: `time.AfterFunc(*timeout, alarm)` となっており、`alarm` という独立した関数がタイムアウト時に呼び出されていました。`alarm` 関数は単に `panic("test timed out")` を実行していました。
    *   **変更後**: `time.AfterFunc` の第2引数に直接無名関数が渡されるようになりました。この無名関数内で `panic(fmt.Sprintf("test timed out after %v", *timeout))` が実行されます。
        *   `fmt.Sprintf("test timed out after %v", *timeout)`: ここが最も重要な変更点です。`*timeout` は `go test -timeout` オプションで指定されたタイムアウト期間を表す `time.Duration` 型の値です。`%v` フォーマット指定子により、この `time.Duration` 値が人間が読める形式(例: "30s", "1m30s")に自動的に変換され、パニックメッセージに埋め込まれます。これにより、タイムアウトが発生した際に、どのくらいの時間でタイムアウトしたのかが明確に表示されるようになり、デバッグの利便性が大幅に向上しました。

3.  **`alarm` 関数の削除**:
    *   `startAlarm` 関数内で無名関数が直接使用されるようになったため、もはや `alarm` 関数は不要となり、コードベースから削除されました。これにより、コードの冗長性が減り、より直接的な実装になりました。

これらの変更は、Goのテストフレームワークの堅牢性とユーザビリティを向上させるための、細かではあるが重要な改善です。特に、タイムアウトメッセージの改善は、開発者がテストの失敗原因を迅速に特定する上で大きな助けとなります。

## 関連リンク

*   Go言語 `testing` パッケージのドキュメント: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
*   Go言語 `time` パッケージのドキュメント: [https://pkg.go.dev/time](https://pkg.go.dev/time)
*   Go言語 `fmt` パッケージのドキュメント: [https://pkg.go.dev/fmt](https://pkg.go.dev/fmt)
*   Go言語の `panic` と `recover` について: [https://go.dev/blog/defer-panic-and-recover](https://go.dev/blog/defer-panic-and-recover)

## 参考にした情報源リンク

*   Goの公式ドキュメント
*   Goのソースコード
*   Goのコミット履歴
*   GoのIssueトラッカー (CL 12252043)
*   一般的なGoプログラミングに関する知識