[インデックス 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言語の概念と一般的なプログラミングの知識が必要です。
-
Go言語のチャネル (Channels):
- チャネルは、Goルーチン間で値を送受信するための通信メカニズムです。Goルーチンは並行処理の単位であり、チャネルはそれらの間の安全なデータ交換を可能にします。
- チャネルは
make(chan Type)
で作成され、ch <- value
で送信、value := <-ch
で受信します。 - クローズされたチャネル (Closed Channel):
close(ch)
関数を使ってチャネルをクローズできます。クローズされたチャネルへの送信はパニック(ランタイムエラー)を引き起こしますが、受信は可能です。クローズされたチャネルからの受信は、チャネルにまだ値が残っていればその値を返し、値がなければ要素型のゼロ値と、チャネルがクローズされたことを示すfalse
のブール値を返します。 closedchan.go
は、クローズされたチャネルからの受信動作や、クローズされたチャネルへの送信がパニックを引き起こすことなどをテストするものです。
-
select
ステートメント:select
ステートメントは、複数のチャネル操作を待機するために使用されます。いずれかのチャネル操作が準備できるまでブロックし、準備できた操作を実行します。case <-ch:
のように受信操作を記述し、case ch <- value:
のように送信操作を記述します。default
ケースを持つこともでき、その場合、どのチャネル操作も準備できていなければすぐにdefault
ケースが実行されます。
-
panic
とrecover
:- Go言語では、予期せぬエラーや回復不能な状況が発生した場合に
panic
を発生させることができます。panic
が発生すると、現在のGoルーチンの実行が停止し、遅延関数(defer
で登録された関数)が実行され、コールスタックを遡ります。 recover
関数は、defer
関数内で呼び出すことで、panic
から回復し、プログラムのクラッシュを防ぐことができます。shouldPanic
関数は、特定のコードブロックがパニックを引き起こすことを期待するテストヘルパー関数です。
- Go言語では、予期せぬエラーや回復不能な状況が発生した場合に
-
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())
のように出力されます。
今回の変更では、このテストの失敗をより明確に外部に通知するために、以下の修正が加えられました。
import "os"
の追加: プログラムの終了コードを制御するためにos
パッケージをインポートします。var failed bool
の導入: テスト中に一つでも失敗条件が検出された場合にtrue
に設定されるグローバル変数failed
を宣言します。- 失敗時の
failed = true
設定: 各テスト関数 (test1
,testasync1
など) 内で、期待と異なる結果(つまりテスト失敗)が検出されたprintln
ステートメントの直後にfailed = true
を追加します。 main
関数での終了コード制御:main
関数の最後に以下のコードブロックを追加します。
これにより、if failed { os.Exit(1) }
failed
変数がtrue
であれば、プログラムは終了コード1
で終了します。これは、テストが失敗したことを意味します。failed
がfalse
のまま(全てのテストが成功)であれば、プログラムはデフォルトで終了コード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)
+ }
}
コアとなるコードの解説
-
import "os"
:main
パッケージの冒頭にos
パッケージがインポートされています。これにより、os.Exit()
関数を使用できるようになります。
-
var failed bool
:main
パッケージのグローバルスコープにfailed
という名前のブール型変数が宣言されています。Go言語では、グローバル変数は明示的に初期化されない場合、その型のゼロ値で初期化されます。ブール型のゼロ値はfalse
なので、failed
はデフォルトでfalse
に設定されます。
-
failed = true
の追加:test1
,testasync1
,testasync2
,testasync3
,testasync4
といったテスト関数内で、println
を使ってエラーメッセージが出力されている箇所(つまり、テストの期待値と実際の値が異なっていた箇所)の直後にfailed = true
が追加されています。- これにより、テストのいずれかの部分で期待される動作と異なる結果が出た場合、
failed
変数がtrue
に設定され、そのテストが失敗したという状態が記録されます。
-
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言語のチャネルに関する公式ドキュメント: https://go.dev/tour/concurrency/2
os
パッケージのドキュメント: https://pkg.go.dev/ospanic
とrecover
について: https://go.dev/blog/defer-panic-and-recover
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(
test/closedchan.go
の変更履歴) - 一般的なUnix系システムの終了コードの慣習に関する知識
- Go言語のテストに関する一般的なプラクティス