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

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

コミット

commit 091ef25a5e8e958dc31c11dd5f281cc9d91ccc66
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jan 10 20:13:02 2012 -0800

    go: rely on exit code to tell if test passed
    
    R=adg, dsymonds
    CC=golang-dev
    https://golang.org/cl/5532066

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

https://github.com/golang/go/commit/091ef25a5e8e958dc31c11dd5f281cc9d91ccc66

元コミット内容

go: rely on exit code to tell if test passed

R=adg, dsymonds
CC=golang-dev
https://golang.org/cl/5532066

変更の背景

このコミットは、Go言語のテスト実行メカニズムにおいて、テストの合否判定方法を改善することを目的としています。以前は、テストが成功したかどうかを判断するために、テスト実行結果の標準出力に特定の文字列(\nPASS\n)が含まれているかどうかをチェックしていました。しかし、この方法は出力形式の変更に脆弱であり、また、コマンドの終了コード(exit code)という、プログラムの成否を示す標準的なメカニズムを十分に活用していませんでした。

この変更の背景には、より堅牢で標準的な方法でテストの合否を判定するという設計思想があります。Unix/Linuxシステムでは、プログラムが正常終了した場合は終了コード0を返し、エラーが発生した場合は非ゼロの終了コードを返すのが一般的です。go testコマンドもこの慣習に従うべきであり、その終了コードを直接参照することで、より信頼性の高い判定が可能になります。

前提知識の解説

終了コード (Exit Code)

終了コード(または終了ステータス、リターンコード)は、プログラムやコマンドが実行を終了した際にオペレーティングシステムに返す数値です。この数値は、そのプログラムの実行が成功したか、あるいはどのような種類のエラーが発生したかを示します。

  • 0: 慣例として、プログラムが正常に終了したことを示します。エラーは発生しませんでした。
  • 非ゼロ (1-255): プログラムの実行中に何らかのエラーが発生したことを示します。具体的な数値は、エラーの種類を示すために使用されることがあります(例: 1は一般的なエラー、2は引数エラーなど)。

シェルスクリプトや他のプログラムは、この終了コードをチェックすることで、先行するコマンドの成否を判断し、それに基づいて次の処理を決定します。例えば、if文でコマンドの終了コードを評価したり、&&(論理AND)や||(論理OR)演算子を使ってコマンドの連結を制御したりします。

go test コマンド

go testは、Go言語の標準的なテスト実行ツールです。Goのソースコード内に記述されたテスト関数(TestXxxという命名規則に従う関数)を検出し、実行します。通常、go testはテストがすべて成功した場合に終了コード0を返し、一つでも失敗したテストがあった場合には非ゼロの終了コードを返します。

bytes.Equalbytes.HasSuffix

Go言語のbytesパッケージは、バイトスライスを操作するためのユーティリティ関数を提供します。

  • bytes.Equal(a, b []byte) bool: 2つのバイトスライスabが完全に等しい場合にtrueを返します。
  • bytes.HasSuffix(s, suffix []byte) bool: バイトスライスsが指定されたsuffixで終わる場合にtrueを返します。

このコミットの変更前は、go testの出力が特定の文字列(\nPASS\n)と完全に一致するか、またはその文字列で終わるかをこれらの関数でチェックしていました。

技術的詳細

このコミットの技術的な核心は、テストの合否判定ロジックを、標準出力の文字列解析から、より信頼性の高いプロセス終了コードの確認へと移行した点にあります。

変更前は、src/cmd/go/test.go内のrunTest関数において、テストバイナリの実行結果(cmd.CombinedOutput()で取得される標準出力と標準エラー出力の結合)をbytes.Equalbytes.HasSuffixを使って解析し、\nPASS\nという文字列が含まれているかどうかでテストの成功を判断していました。

// 変更前
if err == nil && (bytes.Equal(out, pass[1:]) || bytes.HasSuffix(out, pass)) {
    // テスト成功と判断
}

ここで、err == nilcmd.CombinedOutput()がエラーを返さなかった(つまり、テストバイナリが正常に終了した)ことを意味します。しかし、それに加えてout(テストバイナリの出力)がpass\nPASS\n)と一致するか、またはpassで終わるかという条件が追加されていました。

このアプローチにはいくつかの問題点があります。

  1. 脆弱性: テストバイナリの出力形式が将来変更された場合、この文字列解析ロジックが壊れる可能性があります。例えば、PASSの前に余分な情報が出力されたり、PASSの文字列自体が変更されたりすると、正しくテスト結果を判定できなくなります。
  2. 非効率性: 出力全体をバッファリングし、文字列比較を行うオーバーヘッドが発生します。
  3. 非標準的: プログラムの成否は通常、終了コードで示されるべきであり、出力内容で判断するのは一般的ではありません。

このコミットでは、これらの問題を解決するために、テストバイナリの終了コードのみに依存するように変更されました。Goのos/execパッケージ(cmd.CombinedOutput()が内部で使用)では、実行されたコマンドが非ゼロの終了コードを返した場合、errorインターフェースを実装した*exec.ExitError型のエラーが返されます。したがって、err == nilという条件は、テストバイナリが終了コード0で正常終了したことを直接的に意味します。

// 変更後
if err == nil {
    // テスト成功と判断
}

この変更により、go testコマンドは、テストバイナリが終了コード0を返した場合にのみ成功と判断するようになります。これは、Unix/Linuxの標準的な慣習に則った、より堅牢で効率的、かつ予測可能な動作です。

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

変更はsrc/cmd/go/test.goファイルに集中しています。

--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -440,8 +440,6 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,\n 	return pmainAction, runAction, printAction, nil
 }\n 
-var pass = []byte("\nPASS\n")
-\n // runTest is the action for running a test binary.
 func (b *builder) runTest(a *action) error {
 	args := []string{a.deps[0].target}\
@@ -469,7 +467,7 @@ func (b *builder) runTest(a *action) error {\n 	out, err := cmd.CombinedOutput()\n 	t1 := time.Now()\n 	t := fmt.Sprintf("%.3fs", t1.Sub(t0).Seconds())\n-\tif err == nil && (bytes.Equal(out, pass[1:]) || bytes.HasSuffix(out, pass)) {\n+\tif err == nil {\n \t\tfmt.Fprintf(a.testOutput, "ok  \t%s\t%s\n", a.p.ImportPath, t)\n \t\tif testShowPass {\n \t\t\ta.testOutput.Write(out)\n```

具体的には以下の2点が変更されています。

1.  **`pass`変数の削除**:
    ```diff
    -var pass = []byte("\nPASS\n")
    ```
    テストの成功を示す文字列`\nPASS\n`を保持していた`pass`というバイトスライス変数が削除されました。これは、もはやこの文字列を解析する必要がなくなったためです。

2.  **テスト成功判定条件の簡素化**:
    ```diff
    -	if err == nil && (bytes.Equal(out, pass[1:]) || bytes.HasSuffix(out, pass)) {
    +	if err == nil {
    ```
    テストバイナリの実行結果を判定する`if`文の条件が大幅に簡素化されました。以前は`err == nil`に加えて、`out`(テスト出力)が`pass`文字列と一致するか、または`pass`文字列で終わるかという複雑な条件が含まれていましたが、これが単に`err == nil`のみになりました。

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

`src/cmd/go/test.go`内の`runTest`関数は、Goのテストバイナリを実行し、その結果を処理する役割を担っています。

変更前のコードでは、テストバイナリの実行後、`cmd.CombinedOutput()`から返される`err`(エラーオブジェクト)と`out`(標準出力と標準エラー出力の結合)の両方を見ていました。
`err == nil`は、テストバイナリ自体がOSレベルで正常に実行され、終了コード0を返したことを意味します。しかし、それに加えて、テストバイナリがGoのテストフレームワークによって生成された`PASS`という文字列を実際に出力したことを確認するために、`bytes.Equal`や`bytes.HasSuffix`を使って`out`の内容をチェックしていました。これは、テストバイナリが何らかの理由で`PASS`を出力しなかった場合(例えば、パニックを起こしたが終了コードは0だった場合など)に対応するため、あるいは、`go test`コマンドがテストバイナリの内部ロジックに依存して合否を判断していた名残である可能性があります。

このコミットによる変更は、この二重のチェックを排除し、**テストバイナリの終了コードのみを信頼する**という方針を明確にしています。
新しいコード`if err == nil {`は、Goの`os/exec`パッケージのセマンティクスに完全に依存しています。`os/exec`では、実行された外部コマンドが終了コード0で正常終了した場合にのみ`err`が`nil`となります。非ゼロの終了コードを返した場合は、`*exec.ExitError`型のエラーが返されるため、`err`は`nil`ではありません。

したがって、この変更は以下のことを意味します。
*   `go test`コマンドは、テストバイナリが**終了コード0**を返した場合にのみ、そのテストが成功したと判断します。
*   テストバイナリが標準出力に`PASS`という文字列を出力するかどうかは、`go test`コマンドの合否判定ロジックには影響しなくなります。これは、テストバイナリが内部的に`testing`パッケージを使用し、その結果として適切な終了コードを返すことを前提としています。
*   これにより、`go test`のテスト結果判定がより堅牢になり、テストバイナリの出力形式の変更に影響されなくなります。また、Unix/Linuxの標準的なプロセス管理の慣習に沿った、よりクリーンな実装となります。

## 関連リンク

*   Go CL 5532066: [https://golang.org/cl/5532066](https://golang.org/cl/5532066)

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

*   GitHubコミットページ: [https://github.com/golang/go/commit/091ef25a5e8e958dc31c11dd5f281cc9d91ccc66](https://github.com/golang/go/commit/091ef25a5e8e958dc31c11dd5f281cc9d91ccc66)
*   Go言語の`os/exec`パッケージに関する一般的なドキュメント(Go公式ドキュメント)
*   Unix/Linuxにおける終了コードの概念に関する一般的な情報源
*   Go言語の`go test`コマンドに関する一般的なドキュメント(Go公式ドキュメント)
*   Go言語の`bytes`パッケージに関する一般的なドキュメント(Go公式ドキュメント)