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

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

コミット

commit 7f0622e66d5618acc7d2b9ab6e1cb96fd7c1e190
Author: Ian Lance Taylor <iant@golang.org>
Date:   Fri Nov 4 14:12:35 2011 -0700

    test: make closedchan.go exit with failure if something fails
    
    R=golang-dev, rsc, iant
    CC=golang-dev
    https://golang.org/cl/5356042

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

https://github.com/golang/go/commit/7f0622e66d5618acc7d2b9ab6e1cb96fd7c1e190

元コミット内容

このコミットは、Go言語のテストファイル test/closedchan.go を修正し、テストが失敗した場合にプログラムが非ゼロの終了コードで終了するように変更します。これにより、テストハーネスやCI/CDシステムがテストの失敗を正確に検出できるようになります。

変更の背景

Go言語のテストフレームワークでは、テストの成功・失敗は通常、testing パッケージの機能(例: t.Error(), t.Fail(), t.Fatal())を通じて報告されます。しかし、この closedchan.go のような特定のテストスクリプトは、独立したGoプログラムとして実行され、標準出力にメッセージを出力するだけでテストの失敗を通知していました。

このようなテストスクリプトがCI/CD環境や自動テストシステムで実行される場合、標準出力のメッセージを解析してテストの成否を判断するのは非効率的であり、エラーを見落とす可能性もあります。Unix系のシステムでは、プログラムの終了コード(exit code)が0であれば成功、非ゼロであれば失敗を示すのが一般的な慣習です。

このコミットは、closedchan.go がこの慣習に従い、テストが期待通りに動作しなかった場合に明示的に非ゼロの終了コード(1)を返すようにすることで、テストの信頼性と自動化の容易性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と一般的なプログラミングの知識が必要です。

  1. Go言語のチャネル (Channels):

    • チャネルは、Goルーチン間で値を送受信するための通信メカニズムです。Goルーチンは並行処理の単位であり、チャネルはそれらの間の安全なデータ交換を可能にします。
    • チャネルは make(chan Type) で作成され、ch <- value で送信、value := <-ch で受信します。
    • クローズされたチャネル (Closed Channel): close(ch) 関数を使ってチャネルをクローズできます。クローズされたチャネルへの送信はパニック(ランタイムエラー)を引き起こしますが、受信は可能です。クローズされたチャネルからの受信は、チャネルにまだ値が残っていればその値を返し、値がなければ要素型のゼロ値と、チャネルがクローズされたことを示す false のブール値を返します。
    • closedchan.go は、クローズされたチャネルからの受信動作や、クローズされたチャネルへの送信がパニックを引き起こすことなどをテストするものです。
  2. select ステートメント:

    • select ステートメントは、複数のチャネル操作を待機するために使用されます。いずれかのチャネル操作が準備できるまでブロックし、準備できた操作を実行します。
    • case <-ch: のように受信操作を記述し、case ch <- value: のように送信操作を記述します。
    • default ケースを持つこともでき、その場合、どのチャネル操作も準備できていなければすぐに default ケースが実行されます。
  3. panicrecover:

    • Go言語では、予期せぬエラーや回復不能な状況が発生した場合に panic を発生させることができます。panic が発生すると、現在のGoルーチンの実行が停止し、遅延関数(defer で登録された関数)が実行され、コールスタックを遡ります。
    • recover 関数は、defer 関数内で呼び出すことで、panic から回復し、プログラムのクラッシュを防ぐことができます。
    • shouldPanic 関数は、特定のコードブロックがパニックを引き起こすことを期待するテストヘルパー関数です。
  4. os パッケージと os.Exit():

    • os パッケージは、オペレーティングシステムとのインタフェースを提供します。
    • os.Exit(code int) 関数は、現在のプログラムを終了させ、指定された終了コードをオペレーティングシステムに返します。慣例として、0 は成功、1 以上の非ゼロ値はエラーを示します。

技術的詳細

test/closedchan.go は、Go言語のチャネルがクローズされた際の様々な挙動を検証するためのテストファイルです。具体的には、以下のようなシナリオをテストしています。

  • クローズされたチャネルからの受信(値が残っている場合と残っていない場合)。
  • クローズされたチャネルからの非ブロック受信 (Nbrecv, Nbrecv2)。
  • クローズされたチャネルへの送信がパニックを引き起こすこと。
  • select ステートメント内でのクローズされたチャネルの挙動。

このコミット以前は、これらのテストシナリオで期待と異なる結果が出た場合、println 関数を使って標準出力にエラーメッセージを出力するだけでした。例えば、println("test1: recv on closed:", x, c.Impl()) のように出力されます。

今回の変更では、このテストの失敗をより明確に外部に通知するために、以下の修正が加えられました。

  1. import "os" の追加: プログラムの終了コードを制御するために os パッケージをインポートします。
  2. var failed bool の導入: テスト中に一つでも失敗条件が検出された場合に true に設定されるグローバル変数 failed を宣言します。
  3. 失敗時の failed = true 設定: 各テスト関数 (test1, testasync1 など) 内で、期待と異なる結果(つまりテスト失敗)が検出された println ステートメントの直後に failed = true を追加します。
  4. main 関数での終了コード制御: main 関数の最後に以下のコードブロックを追加します。
    if failed {
        os.Exit(1)
    }
    
    これにより、failed 変数が true であれば、プログラムは終了コード 1 で終了します。これは、テストが失敗したことを意味します。failedfalse のまま(全てのテストが成功)であれば、プログラムはデフォルトで終了コード 0 で終了します。

この変更により、closedchan.go を実行するシェルスクリプトやCI/CDパイプラインは、プログラムの終了コードをチェックするだけでテストの成否を判断できるようになり、テストの自動化と信頼性が向上します。

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

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

--- a/test/closedchan.go
+++ b/test/closedchan.go
@@ -11,6 +11,10 @@
 
  package main
 
+import "os"
+
+var failed bool
+
  type Chan interface {
  	Send(int)
  	Nbsend(int) bool
@@ -225,19 +229,23 @@ func test1(c Chan) {
  		// recv a close signal (a zero value)
  		if x := c.Recv(); x != 0 {
  			println("test1: recv on closed:", x, c.Impl())
+			failed = true
  		}
  		if x, ok := c.Recv2(); x != 0 || ok {
  			println("test1: recv2 on closed:", x, ok, c.Impl())
+			failed = true
  		}
 
  		// should work with select: received a value without blocking, so selected == true.
  		x, selected := c.Nbrecv()
  		if x != 0 || !selected {
  			println("test1: recv on closed nb:", x, selected, c.Impl())
+			failed = true
  		}
  		x, ok, selected := c.Nbrecv2()
  		if x != 0 || ok || !selected {
  			println("test1: recv2 on closed nb:", x, ok, selected, c.Impl())
+			failed = true
  		}
  	}
 
@@ -247,12 +255,14 @@ func test1(c Chan) {
  	// the value should have been discarded.
  	if x := c.Recv(); x != 0 {
  		println("test1: recv on closed got non-zero after send on closed:", x, c.Impl())
+		failed = true
  	}
 
  	// similarly Send.
  	shouldPanic(func() { c.Send(2) })
  	if x := c.Recv(); x != 0 {
  		println("test1: recv on closed got non-zero after send on closed:", x, c.Impl())
+		failed = true
  	}
  }
 
@@ -260,6 +270,7 @@ func testasync1(c Chan) {
  	// should be able to get the last value via Recv
  	if x := c.Recv(); x != 1 {
  		println("testasync1: Recv did not get 1:", x, c.Impl())
+		failed = true
  	}
 
  	test1(c)
@@ -269,6 +280,7 @@ func testasync2(c Chan) {
  	// should be able to get the last value via Recv2
  	if x, ok := c.Recv2(); x != 1 || !ok {
  		println("testasync1: Recv did not get 1, true:", x, ok, c.Impl())
+		failed = true
  	}
 
  	test1(c)
@@ -278,6 +290,7 @@ func testasync3(c Chan) {
  	// should be able to get the last value via Nbrecv
  	if x, selected := c.Nbrecv(); x != 1 || !selected {
  		println("testasync2: Nbrecv did not get 1, true:", x, selected, c.Impl())
+		failed = true
  	}
 
  	test1(c)
@@ -287,6 +300,7 @@ func testasync4(c Chan) {
  	// should be able to get the last value via Nbrecv2
  	if x, ok, selected := c.Nbrecv2(); x != 1 || !ok || !selected {
  		println("testasync2: Nbrecv did not get 1, true, true:", x, ok, selected, c.Impl())
+		failed = true
  	}
  	test1(c)
  }
@@ -338,4 +352,8 @@ func main() {
  	shouldPanic(func() {
  		close(ch)
  	})
+
+	if failed {
+		os.Exit(1)
+	}
 }

コアとなるコードの解説

  1. import "os":

    • main パッケージの冒頭に os パッケージがインポートされています。これにより、os.Exit() 関数を使用できるようになります。
  2. var failed bool:

    • main パッケージのグローバルスコープに failed という名前のブール型変数が宣言されています。Go言語では、グローバル変数は明示的に初期化されない場合、その型のゼロ値で初期化されます。ブール型のゼロ値は false なので、failed はデフォルトで false に設定されます。
  3. failed = true の追加:

    • test1, testasync1, testasync2, testasync3, testasync4 といったテスト関数内で、println を使ってエラーメッセージが出力されている箇所(つまり、テストの期待値と実際の値が異なっていた箇所)の直後に failed = true が追加されています。
    • これにより、テストのいずれかの部分で期待される動作と異なる結果が出た場合、failed 変数が true に設定され、そのテストが失敗したという状態が記録されます。
  4. main 関数での os.Exit(1):

    • main 関数の最後に以下のコードブロックが追加されています。
      if failed {
          os.Exit(1)
      }
      
    • main 関数はプログラムのエントリポイントであり、全てのテスト関数が実行された後にこの部分が実行されます。
    • もし failed 変数が true であれば(つまり、テスト中に何らかの失敗が検出されていれば)、os.Exit(1) が呼び出され、プログラムは終了コード 1 で終了します。これは、テストが失敗したことを外部に明確に通知します。
    • もし failed 変数が false のままであれば(つまり、全てのテストが成功していれば)、この if ブロックは実行されず、main 関数は正常に終了し、デフォルトで終了コード 0 を返します。

この一連の変更により、closedchan.go は単なる情報出力だけでなく、プログラムの終了コードを通じてテストの成否を明確に表現する、より堅牢なテストスクリプトとなりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(test/closedchan.go の変更履歴)
  • 一般的なUnix系システムの終了コードの慣習に関する知識
  • Go言語のテストに関する一般的なプラクティス